@kb-labs/review-heuristic 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -0
- package/dist/index.d.ts +240 -0
- package/dist/index.js +500 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# @kb-labs/review-heuristic
|
|
2
|
+
|
|
3
|
+
Heuristic analysis engines for AI Review plugin.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides adapters for deterministic code analysis tools (linters, compilers, SAST) with unified output format and intelligent deduplication.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Engine Registry** - Maps tools to engine types (compiler/linter/sast/ast)
|
|
12
|
+
- **ESLint Adapter** - TypeScript/JavaScript linting with fix templates
|
|
13
|
+
- **Fingerprint Deduplication** - sha1(ruleId|file|bucket|snippetHash)
|
|
14
|
+
- **Engine Type Priority** - compiler > linter > sast > ast > llm
|
|
15
|
+
- **Unified Output** - All engines output ReviewFinding[]
|
|
16
|
+
|
|
17
|
+
## Engine Registry
|
|
18
|
+
|
|
19
|
+
The engine registry maps specific tools to engine types for priority-based deduplication:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { ENGINE_REGISTRY, getEngine, getEngineTypePriority } from '@kb-labs/review-heuristic';
|
|
23
|
+
|
|
24
|
+
// Get engine metadata
|
|
25
|
+
const eslint = getEngine('eslint');
|
|
26
|
+
console.log(eslint.type); // 'linter'
|
|
27
|
+
|
|
28
|
+
// Get priority (lower = higher priority)
|
|
29
|
+
const priority = getEngineTypePriority('eslint');
|
|
30
|
+
console.log(priority); // 2 (linter tier)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Supported Engines
|
|
34
|
+
|
|
35
|
+
| Engine | Type | Language | Priority |
|
|
36
|
+
|--------|------|----------|----------|
|
|
37
|
+
| tsc | compiler | TypeScript | 1 |
|
|
38
|
+
| eslint | linter | TypeScript/JavaScript | 2 |
|
|
39
|
+
| ruff | linter | Python | 2 |
|
|
40
|
+
| golangci | linter | Go | 2 |
|
|
41
|
+
| clippy | linter | Rust | 2 |
|
|
42
|
+
| rubocop | linter | Ruby | 2 |
|
|
43
|
+
| semgrep | sast | Multi-language | 3 |
|
|
44
|
+
| codeql | sast | Multi-language | 3 |
|
|
45
|
+
| bandit | sast | Python | 3 |
|
|
46
|
+
| treesitter | ast | Multi-language | 4 |
|
|
47
|
+
|
|
48
|
+
## ESLint Adapter
|
|
49
|
+
|
|
50
|
+
Run ESLint analysis and get unified findings:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { analyzeWithESLint } from '@kb-labs/review-heuristic';
|
|
54
|
+
|
|
55
|
+
// Quick analysis
|
|
56
|
+
const findings = await analyzeWithESLint(
|
|
57
|
+
['src/**/*.ts', 'src/**/*.tsx'],
|
|
58
|
+
'/path/to/project',
|
|
59
|
+
'/path/to/.eslintrc.json'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console.log(findings);
|
|
63
|
+
// [
|
|
64
|
+
// {
|
|
65
|
+
// id: 'eslint:/path/to/file.ts:no-unused-vars:10:5',
|
|
66
|
+
// ruleId: 'no-unused-vars',
|
|
67
|
+
// type: 'code-quality',
|
|
68
|
+
// severity: 'medium',
|
|
69
|
+
// confidence: 'certain',
|
|
70
|
+
// file: '/path/to/file.ts',
|
|
71
|
+
// line: 10,
|
|
72
|
+
// message: "'foo' is defined but never used.",
|
|
73
|
+
// engine: 'eslint',
|
|
74
|
+
// source: 'heuristic',
|
|
75
|
+
// fix: [{ type: 'replace', ... }],
|
|
76
|
+
// scope: 'local',
|
|
77
|
+
// automated: true
|
|
78
|
+
// }
|
|
79
|
+
// ]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Advanced Usage
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { ESLintAdapter } from '@kb-labs/review-heuristic';
|
|
86
|
+
|
|
87
|
+
const adapter = new ESLintAdapter({
|
|
88
|
+
patterns: ['src/**/*.ts'],
|
|
89
|
+
cwd: '/path/to/project',
|
|
90
|
+
configFile: '.eslintrc.json',
|
|
91
|
+
fix: false, // Don't apply fixes
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const findings = await adapter.analyze();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Deduplication
|
|
98
|
+
|
|
99
|
+
Deduplicate findings from multiple engines using fingerprints and priority:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { deduplicateFindings } from '@kb-labs/review-heuristic';
|
|
103
|
+
|
|
104
|
+
// Findings from multiple engines
|
|
105
|
+
const eslintFindings = await analyzeWithESLint(...);
|
|
106
|
+
const tscFindings = await analyzeWithTSC(...);
|
|
107
|
+
const semgrepFindings = await analyzeWithSemgrep(...);
|
|
108
|
+
|
|
109
|
+
// Combine and deduplicate
|
|
110
|
+
const allFindings = [...eslintFindings, ...tscFindings, ...semgrepFindings];
|
|
111
|
+
const deduplicated = deduplicateFindings(allFindings);
|
|
112
|
+
|
|
113
|
+
// Result: Keeps highest priority finding for each collision
|
|
114
|
+
// - If tsc and eslint report same issue → keep tsc (compiler > linter)
|
|
115
|
+
// - If eslint and semgrep report same issue → keep eslint (linter > sast)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### How Deduplication Works
|
|
119
|
+
|
|
120
|
+
1. **Fingerprint Generation**: `sha1(ruleId|file|bucket|snippetHash)`
|
|
121
|
+
- `ruleId`: Rule identifier (e.g., "no-unused-vars")
|
|
122
|
+
- `file`: File path
|
|
123
|
+
- `bucket`: Line bucket (lines 10-19 → bucket 1)
|
|
124
|
+
- `snippetHash`: Optional hash of code snippet
|
|
125
|
+
|
|
126
|
+
2. **Collision Detection**: Findings with same fingerprint are collisions
|
|
127
|
+
|
|
128
|
+
3. **Priority Resolution**:
|
|
129
|
+
- Sort by engine type priority (compiler > linter > sast > ast > llm)
|
|
130
|
+
- If same type, sort by severity (blocker > high > medium > low > info)
|
|
131
|
+
- Keep highest priority finding, discard rest
|
|
132
|
+
|
|
133
|
+
### Snippet-Based Deduplication
|
|
134
|
+
|
|
135
|
+
For more precise deduplication, use snippet hashes:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { deduplicateFindingsWithSnippets } from '@kb-labs/review-heuristic';
|
|
139
|
+
import { readFile } from 'node:fs/promises';
|
|
140
|
+
|
|
141
|
+
// Function to extract code snippet
|
|
142
|
+
async function getSnippet(finding: ReviewFinding): Promise<string> {
|
|
143
|
+
const content = await readFile(finding.file, 'utf-8');
|
|
144
|
+
const lines = content.split('\n');
|
|
145
|
+
const snippet = lines[finding.line - 1] ?? '';
|
|
146
|
+
return snippet;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Deduplicate with snippets
|
|
150
|
+
const deduplicated = await deduplicateFindingsWithSnippets(
|
|
151
|
+
allFindings,
|
|
152
|
+
getSnippet
|
|
153
|
+
);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Integration
|
|
157
|
+
|
|
158
|
+
This package is used by `@kb-labs/review-core` for heuristic analysis:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// In review-core
|
|
162
|
+
import { analyzeWithESLint, deduplicateFindings } from '@kb-labs/review-heuristic';
|
|
163
|
+
|
|
164
|
+
export async function runHeuristicAnalysis(request: ReviewRequest): Promise<ReviewResult> {
|
|
165
|
+
const findings: ReviewFinding[] = [];
|
|
166
|
+
|
|
167
|
+
// Run ESLint
|
|
168
|
+
const eslintFindings = await analyzeWithESLint(
|
|
169
|
+
request.files,
|
|
170
|
+
request.cwd,
|
|
171
|
+
request.config?.eslintConfig
|
|
172
|
+
);
|
|
173
|
+
findings.push(...eslintFindings);
|
|
174
|
+
|
|
175
|
+
// Run other engines...
|
|
176
|
+
|
|
177
|
+
// Deduplicate
|
|
178
|
+
const deduplicated = deduplicateFindings(findings);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
findings: deduplicated,
|
|
182
|
+
metadata: { ... },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Architecture
|
|
188
|
+
|
|
189
|
+
### Engine Type Priority System
|
|
190
|
+
|
|
191
|
+
Instead of hardcoding priority to specific tools (e.g., "ESLint always wins"), we use **engine type priority**:
|
|
192
|
+
|
|
193
|
+
- **compiler** (priority 1) - Most authoritative, directly from language compiler
|
|
194
|
+
- **linter** (priority 2) - Language-specific best practices and patterns
|
|
195
|
+
- **sast** (priority 3) - Security-focused static analysis
|
|
196
|
+
- **ast** (priority 4) - Read-only AST pattern matching
|
|
197
|
+
- **llm** (priority 5) - LLM-based heuristics (least deterministic)
|
|
198
|
+
|
|
199
|
+
This means:
|
|
200
|
+
- TypeScript compiler (tsc) beats ESLint
|
|
201
|
+
- ESLint beats Semgrep
|
|
202
|
+
- Semgrep beats tree-sitter
|
|
203
|
+
- Any deterministic tool beats LLM
|
|
204
|
+
|
|
205
|
+
But for different languages:
|
|
206
|
+
- `ruff` (Python linter, priority 2) beats `bandit` (Python SAST, priority 3)
|
|
207
|
+
- `golangci-lint` (Go linter, priority 2) beats `semgrep` (SAST, priority 3)
|
|
208
|
+
|
|
209
|
+
### Why This Design?
|
|
210
|
+
|
|
211
|
+
1. **Language-Agnostic** - Works with any language's tooling
|
|
212
|
+
2. **Predictable** - Priority based on engine type, not arbitrary tool ranking
|
|
213
|
+
3. **Extensible** - Easy to add new tools without changing deduplication logic
|
|
214
|
+
4. **Respects Expertise** - Compilers know best, then linters, then SAST
|
|
215
|
+
|
|
216
|
+
## Future Extensions
|
|
217
|
+
|
|
218
|
+
Planned adapters:
|
|
219
|
+
- **Ruff** - Python linter (fast Rust-based)
|
|
220
|
+
- **golangci-lint** - Go meta-linter
|
|
221
|
+
- **Clippy** - Rust linter
|
|
222
|
+
- **RuboCop** - Ruby linter
|
|
223
|
+
- **Semgrep** - Multi-language SAST
|
|
224
|
+
- **CodeQL** - GitHub's SAST engine
|
|
225
|
+
|
|
226
|
+
All will follow the same pattern:
|
|
227
|
+
1. Implement adapter class
|
|
228
|
+
2. Register in ENGINE_REGISTRY
|
|
229
|
+
3. Convert output to ReviewFinding[]
|
|
230
|
+
4. Deduplication handled automatically
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { HeuristicEngine, ReviewFinding } from '@kb-labs/review-contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module @kb-labs/review-heuristic/engine-registry
|
|
5
|
+
* Registry of heuristic analysis engines with type mappings.
|
|
6
|
+
*
|
|
7
|
+
* Maps specific tools (eslint, ruff, clippy) to engine types (linter, compiler, sast).
|
|
8
|
+
* Used for engine type priority deduplication.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registry of all supported heuristic engines.
|
|
13
|
+
*
|
|
14
|
+
* Engine types determine priority in deduplication:
|
|
15
|
+
* - compiler (1): TypeScript compiler, rustc, go build
|
|
16
|
+
* - linter (2): ESLint, Ruff, golangci-lint, Clippy, RuboCop
|
|
17
|
+
* - sast (3): Semgrep, CodeQL, Bandit
|
|
18
|
+
* - ast (4): tree-sitter (read-only AST analysis)
|
|
19
|
+
*/
|
|
20
|
+
declare const ENGINE_REGISTRY: Record<string, HeuristicEngine>;
|
|
21
|
+
/**
|
|
22
|
+
* Get engine by ID.
|
|
23
|
+
*/
|
|
24
|
+
declare function getEngine(engineId: string): HeuristicEngine | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Get all engines supporting a language.
|
|
27
|
+
*/
|
|
28
|
+
declare function getEnginesForLanguage(language: string): HeuristicEngine[];
|
|
29
|
+
/**
|
|
30
|
+
* Get engine type priority for deduplication.
|
|
31
|
+
*
|
|
32
|
+
* Lower number = higher priority (kept in deduplication).
|
|
33
|
+
* - compiler (1) - highest priority
|
|
34
|
+
* - linter (2)
|
|
35
|
+
* - sast (3)
|
|
36
|
+
* - ast (4)
|
|
37
|
+
* - llm (5) - lowest priority
|
|
38
|
+
*/
|
|
39
|
+
declare function getEngineTypePriority(engineId: string): number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @module @kb-labs/review-heuristic/deduplication
|
|
43
|
+
* Fingerprint-based deduplication with engine type priority.
|
|
44
|
+
*
|
|
45
|
+
* Deduplicates findings from multiple engines using:
|
|
46
|
+
* 1. Fingerprint collision detection (sha1(ruleId|file|bucket|snippetHash))
|
|
47
|
+
* 2. Engine type priority (compiler > linter > sast > ast > llm)
|
|
48
|
+
* 3. Severity adjustment (higher severity wins if same type)
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate fingerprint for a finding.
|
|
53
|
+
*
|
|
54
|
+
* Fingerprint = sha1(ruleId|file|bucket|snippetHash)
|
|
55
|
+
* - ruleId: Rule identifier
|
|
56
|
+
* - file: File path
|
|
57
|
+
* - bucket: Line bucket (e.g., lines 10-19 → bucket 1)
|
|
58
|
+
* - snippetHash: Hash of code snippet (optional)
|
|
59
|
+
*/
|
|
60
|
+
declare function generateFingerprint(finding: ReviewFinding, snippetHash?: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Hash code snippet for fingerprint.
|
|
63
|
+
*/
|
|
64
|
+
declare function hashSnippet(snippet: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Deduplicate findings using fingerprints and engine type priority.
|
|
67
|
+
*
|
|
68
|
+
* Algorithm:
|
|
69
|
+
* 1. Group findings by fingerprint
|
|
70
|
+
* 2. For each collision group:
|
|
71
|
+
* - Sort by engine type priority (compiler > linter > sast > ast > llm)
|
|
72
|
+
* - If same type, sort by severity (blocker > high > medium > low > info)
|
|
73
|
+
* - Keep highest priority finding, discard rest
|
|
74
|
+
*
|
|
75
|
+
* @param findings - All findings from all engines
|
|
76
|
+
* @returns Deduplicated findings
|
|
77
|
+
*/
|
|
78
|
+
declare function deduplicateFindings(findings: ReviewFinding[]): ReviewFinding[];
|
|
79
|
+
/**
|
|
80
|
+
* Deduplicate findings with snippet-based fingerprints.
|
|
81
|
+
*
|
|
82
|
+
* More precise deduplication using code snippets.
|
|
83
|
+
*
|
|
84
|
+
* @param findings - All findings
|
|
85
|
+
* @param getSnippet - Function to get code snippet for a finding
|
|
86
|
+
* @returns Deduplicated findings
|
|
87
|
+
*/
|
|
88
|
+
declare function deduplicateFindingsWithSnippets(findings: ReviewFinding[], getSnippet: (finding: ReviewFinding) => Promise<string>): Promise<ReviewFinding[]>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @module @kb-labs/review-heuristic/engines/types
|
|
92
|
+
* Linter engine interface for CLI-based linter integration.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Linter engine definition.
|
|
97
|
+
*
|
|
98
|
+
* Each engine describes how to run a specific linter CLI
|
|
99
|
+
* and parse its JSON output into ReviewFinding[].
|
|
100
|
+
*/
|
|
101
|
+
interface LinterEngine {
|
|
102
|
+
/** Unique engine ID (e.g., 'eslint', 'ruff', 'golangci') */
|
|
103
|
+
id: string;
|
|
104
|
+
/** Human-readable name */
|
|
105
|
+
name: string;
|
|
106
|
+
/** File extensions this linter handles (e.g., ['.ts', '.tsx']) */
|
|
107
|
+
extensions: string[];
|
|
108
|
+
/** Config files to detect project root (e.g., ['eslint.config.js']) */
|
|
109
|
+
configFiles: string[];
|
|
110
|
+
/**
|
|
111
|
+
* Build CLI command to run linter.
|
|
112
|
+
*
|
|
113
|
+
* @param files - Absolute paths to files to lint
|
|
114
|
+
* @param cwd - Working directory (project root)
|
|
115
|
+
* @returns CLI command string
|
|
116
|
+
*/
|
|
117
|
+
buildCommand(files: string[], cwd: string): string;
|
|
118
|
+
/**
|
|
119
|
+
* Parse JSON output from linter CLI.
|
|
120
|
+
*
|
|
121
|
+
* @param json - Raw JSON string from stdout
|
|
122
|
+
* @param cwd - Working directory used for the command
|
|
123
|
+
* @returns Array of ReviewFinding
|
|
124
|
+
*/
|
|
125
|
+
parseOutput(json: string, cwd: string): ReviewFinding[];
|
|
126
|
+
/**
|
|
127
|
+
* Check if linter is available in the system.
|
|
128
|
+
* Optional - if not implemented, assumes available.
|
|
129
|
+
*/
|
|
130
|
+
isAvailable?(): Promise<boolean>;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Result of running a linter.
|
|
134
|
+
*/
|
|
135
|
+
interface LinterResult {
|
|
136
|
+
/** Engine ID */
|
|
137
|
+
engineId: string;
|
|
138
|
+
/** Findings from this linter */
|
|
139
|
+
findings: ReviewFinding[];
|
|
140
|
+
/** Files that were linted */
|
|
141
|
+
files: string[];
|
|
142
|
+
/** Duration in milliseconds */
|
|
143
|
+
durationMs: number;
|
|
144
|
+
/** Error if linter failed completely */
|
|
145
|
+
error?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @module @kb-labs/review-heuristic/engines/eslint
|
|
150
|
+
* ESLint engine - runs ESLint CLI and parses JSON output.
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* ESLint engine definition.
|
|
155
|
+
*/
|
|
156
|
+
declare const eslintEngine: LinterEngine;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @module @kb-labs/review-heuristic/engines/ruff
|
|
160
|
+
* Ruff engine - runs Ruff CLI and parses JSON output.
|
|
161
|
+
*
|
|
162
|
+
* Ruff is an extremely fast Python linter written in Rust.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Ruff engine definition.
|
|
167
|
+
*/
|
|
168
|
+
declare const ruffEngine: LinterEngine;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @module @kb-labs/review-heuristic/engines
|
|
172
|
+
* Linter engine definitions.
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* All available linter engines.
|
|
177
|
+
*/
|
|
178
|
+
declare const LINTER_ENGINES: LinterEngine[];
|
|
179
|
+
/**
|
|
180
|
+
* Get engine by ID.
|
|
181
|
+
*/
|
|
182
|
+
declare function getLinterEngine(id: string): LinterEngine | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* Get engine for file extension.
|
|
185
|
+
*/
|
|
186
|
+
declare function getLinterEngineForFile(filePath: string): LinterEngine | undefined;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @module @kb-labs/review-heuristic/runner
|
|
190
|
+
* Linter runner - executes linters via CLI and collects findings.
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Linter runner configuration.
|
|
195
|
+
*/
|
|
196
|
+
interface LinterRunnerConfig {
|
|
197
|
+
/** Custom engines to use (defaults to all available) */
|
|
198
|
+
engines?: LinterEngine[];
|
|
199
|
+
/** Maximum buffer size for CLI output (default: 10MB) */
|
|
200
|
+
maxBuffer?: number;
|
|
201
|
+
/** Timeout for each linter run in ms (default: 60000) */
|
|
202
|
+
timeout?: number;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Linter runner.
|
|
206
|
+
*
|
|
207
|
+
* Executes linters via CLI subprocess and parses their JSON output.
|
|
208
|
+
*/
|
|
209
|
+
declare class LinterRunner {
|
|
210
|
+
private engines;
|
|
211
|
+
private maxBuffer;
|
|
212
|
+
private timeout;
|
|
213
|
+
constructor(config?: LinterRunnerConfig);
|
|
214
|
+
/**
|
|
215
|
+
* Find project root for a file based on engine's config files.
|
|
216
|
+
*/
|
|
217
|
+
findProjectRoot(file: string, engine: LinterEngine): string;
|
|
218
|
+
/**
|
|
219
|
+
* Run a specific linter on files.
|
|
220
|
+
*/
|
|
221
|
+
runEngine(engineId: string, files: string[]): Promise<LinterResult>;
|
|
222
|
+
/**
|
|
223
|
+
* Run all applicable linters on files.
|
|
224
|
+
*/
|
|
225
|
+
runAll(files: string[]): Promise<LinterResult[]>;
|
|
226
|
+
/**
|
|
227
|
+
* Get all findings from multiple engine results.
|
|
228
|
+
*/
|
|
229
|
+
static collectFindings(results: LinterResult[]): ReviewFinding[];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Quick helper to run all linters on files.
|
|
233
|
+
*/
|
|
234
|
+
declare function runLinters(files: string[]): Promise<ReviewFinding[]>;
|
|
235
|
+
/**
|
|
236
|
+
* Quick helper to run a specific linter.
|
|
237
|
+
*/
|
|
238
|
+
declare function runLinter(engineId: string, files: string[]): Promise<ReviewFinding[]>;
|
|
239
|
+
|
|
240
|
+
export { ENGINE_REGISTRY, LINTER_ENGINES, type LinterEngine, type LinterResult, LinterRunner, type LinterRunnerConfig, deduplicateFindings, deduplicateFindingsWithSnippets, eslintEngine, generateFingerprint, getEngine, getEngineTypePriority, getEnginesForLanguage, getLinterEngine, getLinterEngineForFile, hashSnippet, ruffEngine, runLinter, runLinters };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { dirname, join, extname, relative } from 'path';
|
|
5
|
+
|
|
6
|
+
// src/engine-registry.ts
|
|
7
|
+
var ENGINE_REGISTRY = {
|
|
8
|
+
// JavaScript/TypeScript linters
|
|
9
|
+
eslint: {
|
|
10
|
+
id: "eslint",
|
|
11
|
+
name: "ESLint",
|
|
12
|
+
language: ["typescript", "javascript", "tsx", "jsx"],
|
|
13
|
+
type: "linter"
|
|
14
|
+
},
|
|
15
|
+
// TypeScript compiler
|
|
16
|
+
tsc: {
|
|
17
|
+
id: "tsc",
|
|
18
|
+
name: "TypeScript Compiler",
|
|
19
|
+
language: ["typescript", "tsx"],
|
|
20
|
+
type: "compiler"
|
|
21
|
+
},
|
|
22
|
+
// Python linters
|
|
23
|
+
ruff: {
|
|
24
|
+
id: "ruff",
|
|
25
|
+
name: "Ruff",
|
|
26
|
+
language: ["python"],
|
|
27
|
+
type: "linter"
|
|
28
|
+
},
|
|
29
|
+
pylint: {
|
|
30
|
+
id: "pylint",
|
|
31
|
+
name: "Pylint",
|
|
32
|
+
language: ["python"],
|
|
33
|
+
type: "linter"
|
|
34
|
+
},
|
|
35
|
+
// Python SAST
|
|
36
|
+
bandit: {
|
|
37
|
+
id: "bandit",
|
|
38
|
+
name: "Bandit",
|
|
39
|
+
language: ["python"],
|
|
40
|
+
type: "sast"
|
|
41
|
+
},
|
|
42
|
+
// Go linter
|
|
43
|
+
golangci: {
|
|
44
|
+
id: "golangci",
|
|
45
|
+
name: "golangci-lint",
|
|
46
|
+
language: ["go"],
|
|
47
|
+
type: "linter"
|
|
48
|
+
},
|
|
49
|
+
// Rust linter
|
|
50
|
+
clippy: {
|
|
51
|
+
id: "clippy",
|
|
52
|
+
name: "Clippy",
|
|
53
|
+
language: ["rust"],
|
|
54
|
+
type: "linter"
|
|
55
|
+
},
|
|
56
|
+
// Ruby linter
|
|
57
|
+
rubocop: {
|
|
58
|
+
id: "rubocop",
|
|
59
|
+
name: "RuboCop",
|
|
60
|
+
language: ["ruby"],
|
|
61
|
+
type: "linter"
|
|
62
|
+
},
|
|
63
|
+
// Multi-language SAST
|
|
64
|
+
semgrep: {
|
|
65
|
+
id: "semgrep",
|
|
66
|
+
name: "Semgrep",
|
|
67
|
+
language: ["typescript", "javascript", "python", "go", "rust", "ruby"],
|
|
68
|
+
type: "sast"
|
|
69
|
+
},
|
|
70
|
+
codeql: {
|
|
71
|
+
id: "codeql",
|
|
72
|
+
name: "CodeQL",
|
|
73
|
+
language: ["typescript", "javascript", "python", "go", "rust", "ruby"],
|
|
74
|
+
type: "sast"
|
|
75
|
+
},
|
|
76
|
+
// AST-based analysis (read-only)
|
|
77
|
+
treesitter: {
|
|
78
|
+
id: "treesitter",
|
|
79
|
+
name: "Tree-sitter",
|
|
80
|
+
language: ["typescript", "javascript", "python", "go", "rust", "ruby"],
|
|
81
|
+
type: "ast"
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
function getEngine(engineId) {
|
|
85
|
+
return ENGINE_REGISTRY[engineId];
|
|
86
|
+
}
|
|
87
|
+
function getEnginesForLanguage(language) {
|
|
88
|
+
return Object.values(ENGINE_REGISTRY).filter(
|
|
89
|
+
(engine) => engine.language.includes(language)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
function getEngineTypePriority(engineId) {
|
|
93
|
+
const engine = getEngine(engineId);
|
|
94
|
+
if (!engine) {
|
|
95
|
+
return 999;
|
|
96
|
+
}
|
|
97
|
+
const typePriority = {
|
|
98
|
+
compiler: 1,
|
|
99
|
+
linter: 2,
|
|
100
|
+
sast: 3,
|
|
101
|
+
ast: 4,
|
|
102
|
+
llm: 5
|
|
103
|
+
};
|
|
104
|
+
return typePriority[engine.type] ?? 999;
|
|
105
|
+
}
|
|
106
|
+
function generateFingerprint(finding, snippetHash) {
|
|
107
|
+
const bucket = Math.floor(finding.line / 10);
|
|
108
|
+
const parts = [finding.ruleId, finding.file, bucket.toString()];
|
|
109
|
+
if (snippetHash) {
|
|
110
|
+
parts.push(snippetHash);
|
|
111
|
+
}
|
|
112
|
+
const input = parts.join("|");
|
|
113
|
+
return createHash("sha1").update(input).digest("hex");
|
|
114
|
+
}
|
|
115
|
+
function hashSnippet(snippet) {
|
|
116
|
+
return createHash("sha1").update(snippet.trim()).digest("hex");
|
|
117
|
+
}
|
|
118
|
+
function getSeverityWeight(severity) {
|
|
119
|
+
const weights = {
|
|
120
|
+
blocker: 5,
|
|
121
|
+
high: 4,
|
|
122
|
+
medium: 3,
|
|
123
|
+
low: 2,
|
|
124
|
+
info: 1
|
|
125
|
+
};
|
|
126
|
+
return weights[severity] ?? 0;
|
|
127
|
+
}
|
|
128
|
+
function deduplicateFindings(findings) {
|
|
129
|
+
const fingerprintMap = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const finding of findings) {
|
|
131
|
+
const fingerprint = generateFingerprint(finding);
|
|
132
|
+
if (!fingerprintMap.has(fingerprint)) {
|
|
133
|
+
fingerprintMap.set(fingerprint, []);
|
|
134
|
+
}
|
|
135
|
+
fingerprintMap.get(fingerprint).push(finding);
|
|
136
|
+
}
|
|
137
|
+
const deduplicated = [];
|
|
138
|
+
for (const group of fingerprintMap.values()) {
|
|
139
|
+
if (group.length === 0) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (group.length === 1) {
|
|
143
|
+
deduplicated.push(group[0]);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const sorted = group.sort((a, b) => {
|
|
147
|
+
const aPriority = getEngineTypePriority(a.engine);
|
|
148
|
+
const bPriority = getEngineTypePriority(b.engine);
|
|
149
|
+
if (aPriority !== bPriority) {
|
|
150
|
+
return aPriority - bPriority;
|
|
151
|
+
}
|
|
152
|
+
const aSeverity = getSeverityWeight(a.severity);
|
|
153
|
+
const bSeverity = getSeverityWeight(b.severity);
|
|
154
|
+
return bSeverity - aSeverity;
|
|
155
|
+
});
|
|
156
|
+
const best = sorted[0];
|
|
157
|
+
if (best) {
|
|
158
|
+
deduplicated.push(best);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return deduplicated;
|
|
162
|
+
}
|
|
163
|
+
async function deduplicateFindingsWithSnippets(findings, getSnippet) {
|
|
164
|
+
const fingerprintMap = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const finding of findings) {
|
|
166
|
+
const snippet = await getSnippet(finding);
|
|
167
|
+
const snippetHash = hashSnippet(snippet);
|
|
168
|
+
const fingerprint = generateFingerprint(finding, snippetHash);
|
|
169
|
+
if (!fingerprintMap.has(fingerprint)) {
|
|
170
|
+
fingerprintMap.set(fingerprint, []);
|
|
171
|
+
}
|
|
172
|
+
fingerprintMap.get(fingerprint).push(finding);
|
|
173
|
+
}
|
|
174
|
+
const deduplicated = [];
|
|
175
|
+
for (const group of fingerprintMap.values()) {
|
|
176
|
+
if (group.length === 0) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (group.length === 1) {
|
|
180
|
+
deduplicated.push(group[0]);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const sorted = group.sort((a, b) => {
|
|
184
|
+
const aPriority = getEngineTypePriority(a.engine);
|
|
185
|
+
const bPriority = getEngineTypePriority(b.engine);
|
|
186
|
+
if (aPriority !== bPriority) {
|
|
187
|
+
return aPriority - bPriority;
|
|
188
|
+
}
|
|
189
|
+
const aSeverity = getSeverityWeight(a.severity);
|
|
190
|
+
const bSeverity = getSeverityWeight(b.severity);
|
|
191
|
+
return bSeverity - aSeverity;
|
|
192
|
+
});
|
|
193
|
+
const best = sorted[0];
|
|
194
|
+
if (best) {
|
|
195
|
+
deduplicated.push(best);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return deduplicated;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/engines/eslint.ts
|
|
202
|
+
function mapSeverity(eslintSeverity) {
|
|
203
|
+
if (eslintSeverity === 2) {
|
|
204
|
+
return "high";
|
|
205
|
+
}
|
|
206
|
+
if (eslintSeverity === 1) {
|
|
207
|
+
return "medium";
|
|
208
|
+
}
|
|
209
|
+
return "info";
|
|
210
|
+
}
|
|
211
|
+
var eslintEngine = {
|
|
212
|
+
id: "eslint",
|
|
213
|
+
name: "ESLint",
|
|
214
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
215
|
+
configFiles: [
|
|
216
|
+
"eslint.config.js",
|
|
217
|
+
"eslint.config.mjs",
|
|
218
|
+
"eslint.config.cjs",
|
|
219
|
+
".eslintrc.js",
|
|
220
|
+
".eslintrc.cjs",
|
|
221
|
+
".eslintrc.json",
|
|
222
|
+
".eslintrc.yml",
|
|
223
|
+
".eslintrc.yaml",
|
|
224
|
+
".eslintrc"
|
|
225
|
+
],
|
|
226
|
+
buildCommand(files, _cwd) {
|
|
227
|
+
const quotedFiles = files.map((f) => `"${f}"`).join(" ");
|
|
228
|
+
return `npx eslint --format json ${quotedFiles}`;
|
|
229
|
+
},
|
|
230
|
+
parseOutput(json, _cwd) {
|
|
231
|
+
const results = JSON.parse(json);
|
|
232
|
+
const findings = [];
|
|
233
|
+
for (const result of results) {
|
|
234
|
+
for (const msg of result.messages) {
|
|
235
|
+
findings.push({
|
|
236
|
+
id: `eslint:${result.filePath}:${msg.ruleId ?? "unknown"}:${msg.line}:${msg.column}`,
|
|
237
|
+
ruleId: msg.ruleId ?? "eslint/unknown",
|
|
238
|
+
type: "code-quality",
|
|
239
|
+
severity: mapSeverity(msg.severity),
|
|
240
|
+
confidence: "certain",
|
|
241
|
+
file: result.filePath,
|
|
242
|
+
line: msg.line ?? 1,
|
|
243
|
+
column: msg.column ?? 1,
|
|
244
|
+
endLine: msg.endLine ?? msg.line ?? 1,
|
|
245
|
+
message: msg.message,
|
|
246
|
+
engine: "eslint",
|
|
247
|
+
source: "heuristic",
|
|
248
|
+
scope: msg.fix ? "local" : "global",
|
|
249
|
+
automated: !!msg.fix
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return findings;
|
|
254
|
+
},
|
|
255
|
+
async isAvailable() {
|
|
256
|
+
try {
|
|
257
|
+
const { execSync: execSync2 } = await import('child_process');
|
|
258
|
+
execSync2("npx eslint --version", { stdio: "pipe" });
|
|
259
|
+
return true;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// src/engines/ruff.ts
|
|
267
|
+
function mapSeverity2(code) {
|
|
268
|
+
const prefix = code.charAt(0).toUpperCase();
|
|
269
|
+
switch (prefix) {
|
|
270
|
+
case "E":
|
|
271
|
+
// Error
|
|
272
|
+
case "F":
|
|
273
|
+
return "high";
|
|
274
|
+
case "W":
|
|
275
|
+
// Warning
|
|
276
|
+
case "C":
|
|
277
|
+
return "medium";
|
|
278
|
+
case "I":
|
|
279
|
+
// Isort
|
|
280
|
+
case "D":
|
|
281
|
+
return "low";
|
|
282
|
+
default:
|
|
283
|
+
return "medium";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
var ruffEngine = {
|
|
287
|
+
id: "ruff",
|
|
288
|
+
name: "Ruff",
|
|
289
|
+
extensions: [".py", ".pyi"],
|
|
290
|
+
configFiles: [
|
|
291
|
+
"pyproject.toml",
|
|
292
|
+
"ruff.toml",
|
|
293
|
+
".ruff.toml"
|
|
294
|
+
],
|
|
295
|
+
buildCommand(files, _cwd) {
|
|
296
|
+
const quotedFiles = files.map((f) => `"${f}"`).join(" ");
|
|
297
|
+
return `ruff check --output-format json ${quotedFiles}`;
|
|
298
|
+
},
|
|
299
|
+
parseOutput(json, _cwd) {
|
|
300
|
+
const diagnostics = JSON.parse(json);
|
|
301
|
+
const findings = [];
|
|
302
|
+
for (const d of diagnostics) {
|
|
303
|
+
findings.push({
|
|
304
|
+
id: `ruff:${d.filename}:${d.code}:${d.location.row}:${d.location.column}`,
|
|
305
|
+
ruleId: d.code,
|
|
306
|
+
type: "code-quality",
|
|
307
|
+
severity: mapSeverity2(d.code),
|
|
308
|
+
confidence: "certain",
|
|
309
|
+
file: d.filename,
|
|
310
|
+
line: d.location.row,
|
|
311
|
+
column: d.location.column,
|
|
312
|
+
endLine: d.end_location.row,
|
|
313
|
+
message: d.message,
|
|
314
|
+
engine: "ruff",
|
|
315
|
+
source: "heuristic",
|
|
316
|
+
scope: d.fix ? "local" : "global",
|
|
317
|
+
automated: !!d.fix
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return findings;
|
|
321
|
+
},
|
|
322
|
+
async isAvailable() {
|
|
323
|
+
try {
|
|
324
|
+
const { execSync: execSync2 } = await import('child_process');
|
|
325
|
+
execSync2("ruff --version", { stdio: "pipe" });
|
|
326
|
+
return true;
|
|
327
|
+
} catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/engines/index.ts
|
|
334
|
+
var LINTER_ENGINES = [
|
|
335
|
+
eslintEngine,
|
|
336
|
+
ruffEngine
|
|
337
|
+
];
|
|
338
|
+
function getLinterEngine(id) {
|
|
339
|
+
return LINTER_ENGINES.find((e) => e.id === id);
|
|
340
|
+
}
|
|
341
|
+
function getLinterEngineForFile(filePath) {
|
|
342
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
343
|
+
return LINTER_ENGINES.find((e) => e.extensions.includes(ext));
|
|
344
|
+
}
|
|
345
|
+
var LinterRunner = class {
|
|
346
|
+
engines;
|
|
347
|
+
maxBuffer;
|
|
348
|
+
timeout;
|
|
349
|
+
constructor(config = {}) {
|
|
350
|
+
const engineList = config.engines ?? LINTER_ENGINES;
|
|
351
|
+
this.engines = new Map(engineList.map((e) => [e.id, e]));
|
|
352
|
+
this.maxBuffer = config.maxBuffer ?? 10 * 1024 * 1024;
|
|
353
|
+
this.timeout = config.timeout ?? 6e4;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Find project root for a file based on engine's config files.
|
|
357
|
+
*/
|
|
358
|
+
findProjectRoot(file, engine) {
|
|
359
|
+
let dir = dirname(file);
|
|
360
|
+
const root = "/";
|
|
361
|
+
while (dir !== root && dir.length > 1) {
|
|
362
|
+
for (const configFile of engine.configFiles) {
|
|
363
|
+
if (existsSync(join(dir, configFile))) {
|
|
364
|
+
return dir;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const parent = dirname(dir);
|
|
368
|
+
if (parent === dir) {
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
dir = parent;
|
|
372
|
+
}
|
|
373
|
+
return dirname(file);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Run a specific linter on files.
|
|
377
|
+
*/
|
|
378
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex orchestration of linter execution with error handling, config discovery, and result processing
|
|
379
|
+
async runEngine(engineId, files) {
|
|
380
|
+
const startTime = Date.now();
|
|
381
|
+
const engine = this.engines.get(engineId);
|
|
382
|
+
if (!engine) {
|
|
383
|
+
return {
|
|
384
|
+
engineId,
|
|
385
|
+
findings: [],
|
|
386
|
+
files,
|
|
387
|
+
durationMs: Date.now() - startTime,
|
|
388
|
+
error: `Unknown engine: ${engineId}`
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const relevantFiles = files.filter(
|
|
392
|
+
(f) => engine.extensions.includes(extname(f))
|
|
393
|
+
);
|
|
394
|
+
if (relevantFiles.length === 0) {
|
|
395
|
+
return {
|
|
396
|
+
engineId,
|
|
397
|
+
findings: [],
|
|
398
|
+
files: [],
|
|
399
|
+
durationMs: Date.now() - startTime
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const filesByRoot = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const file of relevantFiles) {
|
|
404
|
+
const root = this.findProjectRoot(file, engine);
|
|
405
|
+
const group = filesByRoot.get(root) ?? [];
|
|
406
|
+
group.push(file);
|
|
407
|
+
filesByRoot.set(root, group);
|
|
408
|
+
}
|
|
409
|
+
const allFindings = [];
|
|
410
|
+
const errors = [];
|
|
411
|
+
for (const [projectRoot, groupFiles] of filesByRoot) {
|
|
412
|
+
try {
|
|
413
|
+
const relativeFiles = groupFiles.map((f) => {
|
|
414
|
+
if (f.startsWith(projectRoot + "/")) {
|
|
415
|
+
return f.slice(projectRoot.length + 1);
|
|
416
|
+
}
|
|
417
|
+
return relative(projectRoot, f) || f;
|
|
418
|
+
});
|
|
419
|
+
const command = engine.buildCommand(relativeFiles, projectRoot);
|
|
420
|
+
let output;
|
|
421
|
+
try {
|
|
422
|
+
output = execSync(command, {
|
|
423
|
+
cwd: projectRoot,
|
|
424
|
+
encoding: "utf-8",
|
|
425
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
426
|
+
maxBuffer: this.maxBuffer,
|
|
427
|
+
timeout: this.timeout
|
|
428
|
+
});
|
|
429
|
+
} catch (execError) {
|
|
430
|
+
const err = execError;
|
|
431
|
+
if (err.stdout) {
|
|
432
|
+
output = err.stdout;
|
|
433
|
+
} else {
|
|
434
|
+
errors.push(`${projectRoot}: ${err.message ?? "Unknown error"}`);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (output && output.trim()) {
|
|
439
|
+
try {
|
|
440
|
+
const findings = engine.parseOutput(output, projectRoot);
|
|
441
|
+
allFindings.push(...findings);
|
|
442
|
+
} catch (parseError) {
|
|
443
|
+
const err = parseError;
|
|
444
|
+
errors.push(`${projectRoot}: Failed to parse output: ${err.message ?? "Unknown error"}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
const err = error;
|
|
449
|
+
errors.push(`${projectRoot}: ${err.message ?? "Unknown error"}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
engineId,
|
|
454
|
+
findings: allFindings,
|
|
455
|
+
files: relevantFiles,
|
|
456
|
+
durationMs: Date.now() - startTime,
|
|
457
|
+
error: errors.length > 0 ? errors.join("; ") : void 0
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Run all applicable linters on files.
|
|
462
|
+
*/
|
|
463
|
+
async runAll(files) {
|
|
464
|
+
const results = [];
|
|
465
|
+
const enginesWithFiles = /* @__PURE__ */ new Set();
|
|
466
|
+
for (const file of files) {
|
|
467
|
+
const ext = extname(file);
|
|
468
|
+
for (const engine of this.engines.values()) {
|
|
469
|
+
if (engine.extensions.includes(ext)) {
|
|
470
|
+
enginesWithFiles.add(engine.id);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (const engineId of enginesWithFiles) {
|
|
475
|
+
const result = await this.runEngine(engineId, files);
|
|
476
|
+
results.push(result);
|
|
477
|
+
}
|
|
478
|
+
return results;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get all findings from multiple engine results.
|
|
482
|
+
*/
|
|
483
|
+
static collectFindings(results) {
|
|
484
|
+
return results.flatMap((r) => r.findings);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
async function runLinters(files) {
|
|
488
|
+
const runner = new LinterRunner();
|
|
489
|
+
const results = await runner.runAll(files);
|
|
490
|
+
return LinterRunner.collectFindings(results);
|
|
491
|
+
}
|
|
492
|
+
async function runLinter(engineId, files) {
|
|
493
|
+
const runner = new LinterRunner();
|
|
494
|
+
const result = await runner.runEngine(engineId, files);
|
|
495
|
+
return result.findings;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export { ENGINE_REGISTRY, LINTER_ENGINES, LinterRunner, deduplicateFindings, deduplicateFindingsWithSnippets, eslintEngine, generateFingerprint, getEngine, getEngineTypePriority, getEnginesForLanguage, getLinterEngine, getLinterEngineForFile, hashSnippet, ruffEngine, runLinter, runLinters };
|
|
499
|
+
//# sourceMappingURL=index.js.map
|
|
500
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engine-registry.ts","../src/deduplication.ts","../src/engines/eslint.ts","../src/engines/ruff.ts","../src/engines/index.ts","../src/runner.ts"],"names":["execSync","mapSeverity"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @module @kb-labs/review-heuristic/engine-registry\n * Registry of heuristic analysis engines with type mappings.\n *\n * Maps specific tools (eslint, ruff, clippy) to engine types (linter, compiler, sast).\n * Used for engine type priority deduplication.\n */\n\nimport type { HeuristicEngine } from '@kb-labs/review-contracts';\n\n/**\n * Registry of all supported heuristic engines.\n *\n * Engine types determine priority in deduplication:\n * - compiler (1): TypeScript compiler, rustc, go build\n * - linter (2): ESLint, Ruff, golangci-lint, Clippy, RuboCop\n * - sast (3): Semgrep, CodeQL, Bandit\n * - ast (4): tree-sitter (read-only AST analysis)\n */\nexport const ENGINE_REGISTRY: Record<string, HeuristicEngine> = {\n // JavaScript/TypeScript linters\n eslint: {\n id: 'eslint',\n name: 'ESLint',\n language: ['typescript', 'javascript', 'tsx', 'jsx'],\n type: 'linter',\n },\n\n // TypeScript compiler\n tsc: {\n id: 'tsc',\n name: 'TypeScript Compiler',\n language: ['typescript', 'tsx'],\n type: 'compiler',\n },\n\n // Python linters\n ruff: {\n id: 'ruff',\n name: 'Ruff',\n language: ['python'],\n type: 'linter',\n },\n\n pylint: {\n id: 'pylint',\n name: 'Pylint',\n language: ['python'],\n type: 'linter',\n },\n\n // Python SAST\n bandit: {\n id: 'bandit',\n name: 'Bandit',\n language: ['python'],\n type: 'sast',\n },\n\n // Go linter\n golangci: {\n id: 'golangci',\n name: 'golangci-lint',\n language: ['go'],\n type: 'linter',\n },\n\n // Rust linter\n clippy: {\n id: 'clippy',\n name: 'Clippy',\n language: ['rust'],\n type: 'linter',\n },\n\n // Ruby linter\n rubocop: {\n id: 'rubocop',\n name: 'RuboCop',\n language: ['ruby'],\n type: 'linter',\n },\n\n // Multi-language SAST\n semgrep: {\n id: 'semgrep',\n name: 'Semgrep',\n language: ['typescript', 'javascript', 'python', 'go', 'rust', 'ruby'],\n type: 'sast',\n },\n\n codeql: {\n id: 'codeql',\n name: 'CodeQL',\n language: ['typescript', 'javascript', 'python', 'go', 'rust', 'ruby'],\n type: 'sast',\n },\n\n // AST-based analysis (read-only)\n treesitter: {\n id: 'treesitter',\n name: 'Tree-sitter',\n language: ['typescript', 'javascript', 'python', 'go', 'rust', 'ruby'],\n type: 'ast',\n },\n};\n\n/**\n * Get engine by ID.\n */\nexport function getEngine(engineId: string): HeuristicEngine | undefined {\n return ENGINE_REGISTRY[engineId];\n}\n\n/**\n * Get all engines supporting a language.\n */\nexport function getEnginesForLanguage(language: string): HeuristicEngine[] {\n return Object.values(ENGINE_REGISTRY).filter((engine) =>\n engine.language.includes(language)\n );\n}\n\n/**\n * Get engine type priority for deduplication.\n *\n * Lower number = higher priority (kept in deduplication).\n * - compiler (1) - highest priority\n * - linter (2)\n * - sast (3)\n * - ast (4)\n * - llm (5) - lowest priority\n */\nexport function getEngineTypePriority(engineId: string): number {\n const engine = getEngine(engineId);\n if (!engine) {\n // Unknown engine, treat as lowest priority\n return 999;\n }\n\n const typePriority: Record<string, number> = {\n compiler: 1,\n linter: 2,\n sast: 3,\n ast: 4,\n llm: 5,\n };\n\n return typePriority[engine.type] ?? 999;\n}\n","/**\n * @module @kb-labs/review-heuristic/deduplication\n * Fingerprint-based deduplication with engine type priority.\n *\n * Deduplicates findings from multiple engines using:\n * 1. Fingerprint collision detection (sha1(ruleId|file|bucket|snippetHash))\n * 2. Engine type priority (compiler > linter > sast > ast > llm)\n * 3. Severity adjustment (higher severity wins if same type)\n */\n\nimport { createHash } from 'node:crypto';\nimport type { ReviewFinding, FindingSeverity } from '@kb-labs/review-contracts';\nimport { getEngineTypePriority } from './engine-registry.js';\n\n/**\n * Generate fingerprint for a finding.\n *\n * Fingerprint = sha1(ruleId|file|bucket|snippetHash)\n * - ruleId: Rule identifier\n * - file: File path\n * - bucket: Line bucket (e.g., lines 10-19 → bucket 1)\n * - snippetHash: Hash of code snippet (optional)\n */\nexport function generateFingerprint(\n finding: ReviewFinding,\n snippetHash?: string\n): string {\n const bucket = Math.floor(finding.line / 10);\n const parts = [finding.ruleId, finding.file, bucket.toString()];\n\n if (snippetHash) {\n parts.push(snippetHash);\n }\n\n const input = parts.join('|');\n return createHash('sha1').update(input).digest('hex');\n}\n\n/**\n * Hash code snippet for fingerprint.\n */\nexport function hashSnippet(snippet: string): string {\n return createHash('sha1').update(snippet.trim()).digest('hex');\n}\n\n/**\n * Get severity weight for comparison.\n *\n * Higher number = more severe.\n */\nfunction getSeverityWeight(severity: FindingSeverity): number {\n const weights: Record<FindingSeverity, number> = {\n blocker: 5,\n high: 4,\n medium: 3,\n low: 2,\n info: 1,\n };\n return weights[severity] ?? 0;\n}\n\n/**\n * Deduplicate findings using fingerprints and engine type priority.\n *\n * Algorithm:\n * 1. Group findings by fingerprint\n * 2. For each collision group:\n * - Sort by engine type priority (compiler > linter > sast > ast > llm)\n * - If same type, sort by severity (blocker > high > medium > low > info)\n * - Keep highest priority finding, discard rest\n *\n * @param findings - All findings from all engines\n * @returns Deduplicated findings\n */\nexport function deduplicateFindings(findings: ReviewFinding[]): ReviewFinding[] {\n // Generate fingerprints\n const fingerprintMap = new Map<string, ReviewFinding[]>();\n\n for (const finding of findings) {\n const fingerprint = generateFingerprint(finding);\n\n if (!fingerprintMap.has(fingerprint)) {\n fingerprintMap.set(fingerprint, []);\n }\n\n fingerprintMap.get(fingerprint)!.push(finding);\n }\n\n // Deduplicate each collision group\n const deduplicated: ReviewFinding[] = [];\n\n for (const group of fingerprintMap.values()) {\n if (group.length === 0) {\n continue;\n }\n\n if (group.length === 1) {\n // No collision, keep as-is\n deduplicated.push(group[0]!);\n continue;\n }\n\n // Sort by priority\n const sorted = group.sort((a, b) => {\n // 1. Engine type priority (lower number = higher priority)\n const aPriority = getEngineTypePriority(a.engine);\n const bPriority = getEngineTypePriority(b.engine);\n\n if (aPriority !== bPriority) {\n return aPriority - bPriority;\n }\n\n // 2. Severity (higher severity wins)\n const aSeverity = getSeverityWeight(a.severity);\n const bSeverity = getSeverityWeight(b.severity);\n\n return bSeverity - aSeverity;\n });\n\n // Keep highest priority finding\n const best = sorted[0];\n if (best) {\n deduplicated.push(best);\n }\n }\n\n return deduplicated;\n}\n\n/**\n * Deduplicate findings with snippet-based fingerprints.\n *\n * More precise deduplication using code snippets.\n *\n * @param findings - All findings\n * @param getSnippet - Function to get code snippet for a finding\n * @returns Deduplicated findings\n */\nexport async function deduplicateFindingsWithSnippets(\n findings: ReviewFinding[],\n getSnippet: (finding: ReviewFinding) => Promise<string>\n): Promise<ReviewFinding[]> {\n // Generate fingerprints with snippets\n const fingerprintMap = new Map<string, ReviewFinding[]>();\n\n for (const finding of findings) {\n // eslint-disable-next-line no-await-in-loop -- Sequential snippet fetching for fingerprinting\n const snippet = await getSnippet(finding);\n const snippetHash = hashSnippet(snippet);\n const fingerprint = generateFingerprint(finding, snippetHash);\n\n if (!fingerprintMap.has(fingerprint)) {\n fingerprintMap.set(fingerprint, []);\n }\n\n fingerprintMap.get(fingerprint)!.push(finding);\n }\n\n // Deduplicate each collision group\n const deduplicated: ReviewFinding[] = [];\n\n for (const group of fingerprintMap.values()) {\n if (group.length === 0) {\n continue;\n }\n\n if (group.length === 1) {\n deduplicated.push(group[0]!);\n continue;\n }\n\n // Sort by priority\n const sorted = group.sort((a, b) => {\n const aPriority = getEngineTypePriority(a.engine);\n const bPriority = getEngineTypePriority(b.engine);\n\n if (aPriority !== bPriority) {\n return aPriority - bPriority;\n }\n\n const aSeverity = getSeverityWeight(a.severity);\n const bSeverity = getSeverityWeight(b.severity);\n\n return bSeverity - aSeverity;\n });\n\n const best = sorted[0];\n if (best) {\n deduplicated.push(best);\n }\n }\n\n return deduplicated;\n}\n","/**\n * @module @kb-labs/review-heuristic/engines/eslint\n * ESLint engine - runs ESLint CLI and parses JSON output.\n */\n\nimport type { ReviewFinding, FindingSeverity } from '@kb-labs/review-contracts';\nimport type { LinterEngine } from './types.js';\n\n/**\n * Map ESLint severity (1=warn, 2=error) to ReviewFinding severity.\n */\nfunction mapSeverity(eslintSeverity: number): FindingSeverity {\n if (eslintSeverity === 2) {\n return 'high';\n }\n if (eslintSeverity === 1) {\n return 'medium';\n }\n return 'info';\n}\n\n/**\n * ESLint JSON output format (per file).\n */\ninterface ESLintFileResult {\n filePath: string;\n messages: Array<{\n ruleId: string | null;\n severity: number;\n message: string;\n line: number;\n column: number;\n endLine?: number;\n endColumn?: number;\n fix?: {\n range: [number, number];\n text: string;\n };\n }>;\n errorCount: number;\n warningCount: number;\n}\n\n/**\n * ESLint engine definition.\n */\nexport const eslintEngine: LinterEngine = {\n id: 'eslint',\n name: 'ESLint',\n extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'],\n configFiles: [\n 'eslint.config.js',\n 'eslint.config.mjs',\n 'eslint.config.cjs',\n '.eslintrc.js',\n '.eslintrc.cjs',\n '.eslintrc.json',\n '.eslintrc.yml',\n '.eslintrc.yaml',\n '.eslintrc',\n ],\n\n buildCommand(files: string[], _cwd: string): string {\n // Quote each file path to handle spaces\n const quotedFiles = files.map(f => `\"${f}\"`).join(' ');\n return `npx eslint --format json ${quotedFiles}`;\n },\n\n parseOutput(json: string, _cwd: string): ReviewFinding[] {\n const results: ESLintFileResult[] = JSON.parse(json);\n const findings: ReviewFinding[] = [];\n\n for (const result of results) {\n for (const msg of result.messages) {\n findings.push({\n id: `eslint:${result.filePath}:${msg.ruleId ?? 'unknown'}:${msg.line}:${msg.column}`,\n ruleId: msg.ruleId ?? 'eslint/unknown',\n type: 'code-quality',\n severity: mapSeverity(msg.severity),\n confidence: 'certain',\n file: result.filePath,\n line: msg.line ?? 1,\n column: msg.column ?? 1,\n endLine: msg.endLine ?? msg.line ?? 1,\n message: msg.message,\n engine: 'eslint',\n source: 'heuristic',\n scope: msg.fix ? 'local' : 'global',\n automated: !!msg.fix,\n });\n }\n }\n\n return findings;\n },\n\n async isAvailable(): Promise<boolean> {\n try {\n const { execSync } = await import('child_process');\n execSync('npx eslint --version', { stdio: 'pipe' });\n return true;\n } catch {\n return false;\n }\n },\n};\n","/**\n * @module @kb-labs/review-heuristic/engines/ruff\n * Ruff engine - runs Ruff CLI and parses JSON output.\n *\n * Ruff is an extremely fast Python linter written in Rust.\n */\n\nimport type { ReviewFinding, FindingSeverity } from '@kb-labs/review-contracts';\nimport type { LinterEngine } from './types.js';\n\n/**\n * Ruff JSON output format (per diagnostic).\n */\ninterface RuffDiagnostic {\n code: string;\n message: string;\n filename: string;\n location: {\n row: number;\n column: number;\n };\n end_location: {\n row: number;\n column: number;\n };\n fix?: {\n message: string;\n edits: Array<{\n content: string;\n location: { row: number; column: number };\n end_location: { row: number; column: number };\n }>;\n };\n noqa_row?: number;\n}\n\n/**\n * Map Ruff code prefix to severity.\n * E = Error, W = Warning, F = Fatal, etc.\n */\nfunction mapSeverity(code: string): FindingSeverity {\n const prefix = code.charAt(0).toUpperCase();\n\n switch (prefix) {\n case 'E': // Error\n case 'F': // Pyflakes\n return 'high';\n case 'W': // Warning\n case 'C': // Convention\n return 'medium';\n case 'I': // Isort\n case 'D': // Docstring\n return 'low';\n default:\n return 'medium';\n }\n}\n\n/**\n * Ruff engine definition.\n */\nexport const ruffEngine: LinterEngine = {\n id: 'ruff',\n name: 'Ruff',\n extensions: ['.py', '.pyi'],\n configFiles: [\n 'pyproject.toml',\n 'ruff.toml',\n '.ruff.toml',\n ],\n\n buildCommand(files: string[], _cwd: string): string {\n const quotedFiles = files.map(f => `\"${f}\"`).join(' ');\n return `ruff check --output-format json ${quotedFiles}`;\n },\n\n parseOutput(json: string, _cwd: string): ReviewFinding[] {\n const diagnostics: RuffDiagnostic[] = JSON.parse(json);\n const findings: ReviewFinding[] = [];\n\n for (const d of diagnostics) {\n findings.push({\n id: `ruff:${d.filename}:${d.code}:${d.location.row}:${d.location.column}`,\n ruleId: d.code,\n type: 'code-quality',\n severity: mapSeverity(d.code),\n confidence: 'certain',\n file: d.filename,\n line: d.location.row,\n column: d.location.column,\n endLine: d.end_location.row,\n message: d.message,\n engine: 'ruff',\n source: 'heuristic',\n scope: d.fix ? 'local' : 'global',\n automated: !!d.fix,\n });\n }\n\n return findings;\n },\n\n async isAvailable(): Promise<boolean> {\n try {\n const { execSync } = await import('child_process');\n execSync('ruff --version', { stdio: 'pipe' });\n return true;\n } catch {\n return false;\n }\n },\n};\n","/**\n * @module @kb-labs/review-heuristic/engines\n * Linter engine definitions.\n */\n\nexport type { LinterEngine, LinterResult } from './types.js';\nexport { eslintEngine } from './eslint.js';\nexport { ruffEngine } from './ruff.js';\n\nimport { eslintEngine } from './eslint.js';\nimport { ruffEngine } from './ruff.js';\nimport type { LinterEngine } from './types.js';\n\n/**\n * All available linter engines.\n */\nexport const LINTER_ENGINES: LinterEngine[] = [\n eslintEngine,\n ruffEngine,\n];\n\n/**\n * Get engine by ID.\n */\nexport function getLinterEngine(id: string): LinterEngine | undefined {\n return LINTER_ENGINES.find(e => e.id === id);\n}\n\n/**\n * Get engine for file extension.\n */\nexport function getLinterEngineForFile(filePath: string): LinterEngine | undefined {\n const ext = filePath.slice(filePath.lastIndexOf('.'));\n return LINTER_ENGINES.find(e => e.extensions.includes(ext));\n}\n","/**\n * @module @kb-labs/review-heuristic/runner\n * Linter runner - executes linters via CLI and collects findings.\n */\n\nimport { execSync } from 'child_process';\nimport { existsSync } from 'fs';\nimport { dirname, join, extname, relative } from 'path';\nimport type { ReviewFinding } from '@kb-labs/review-contracts';\nimport type { LinterEngine, LinterResult } from './engines/types.js';\nimport { LINTER_ENGINES } from './engines/index.js';\n\n/**\n * Linter runner configuration.\n */\nexport interface LinterRunnerConfig {\n /** Custom engines to use (defaults to all available) */\n engines?: LinterEngine[];\n\n /** Maximum buffer size for CLI output (default: 10MB) */\n maxBuffer?: number;\n\n /** Timeout for each linter run in ms (default: 60000) */\n timeout?: number;\n}\n\n/**\n * Linter runner.\n *\n * Executes linters via CLI subprocess and parses their JSON output.\n */\nexport class LinterRunner {\n private engines: Map<string, LinterEngine>;\n private maxBuffer: number;\n private timeout: number;\n\n constructor(config: LinterRunnerConfig = {}) {\n const engineList = config.engines ?? LINTER_ENGINES;\n this.engines = new Map(engineList.map(e => [e.id, e]));\n this.maxBuffer = config.maxBuffer ?? 10 * 1024 * 1024; // 10MB\n this.timeout = config.timeout ?? 60000; // 60s\n }\n\n /**\n * Find project root for a file based on engine's config files.\n */\n findProjectRoot(file: string, engine: LinterEngine): string {\n let dir = dirname(file);\n const root = '/';\n\n while (dir !== root && dir.length > 1) {\n for (const configFile of engine.configFiles) {\n if (existsSync(join(dir, configFile))) {\n return dir;\n }\n }\n const parent = dirname(dir);\n if (parent === dir) {\n break;\n }\n dir = parent;\n }\n\n // Fallback to file's directory\n return dirname(file);\n }\n\n /**\n * Run a specific linter on files.\n */\n // eslint-disable-next-line sonarjs/cognitive-complexity -- Complex orchestration of linter execution with error handling, config discovery, and result processing\n async runEngine(engineId: string, files: string[]): Promise<LinterResult> {\n const startTime = Date.now();\n const engine = this.engines.get(engineId);\n\n if (!engine) {\n return {\n engineId,\n findings: [],\n files,\n durationMs: Date.now() - startTime,\n error: `Unknown engine: ${engineId}`,\n };\n }\n\n // Filter files by extension\n const relevantFiles = files.filter(f =>\n engine.extensions.includes(extname(f))\n );\n\n if (relevantFiles.length === 0) {\n return {\n engineId,\n findings: [],\n files: [],\n durationMs: Date.now() - startTime,\n };\n }\n\n // Group files by project root\n const filesByRoot = new Map<string, string[]>();\n for (const file of relevantFiles) {\n const root = this.findProjectRoot(file, engine);\n const group = filesByRoot.get(root) ?? [];\n group.push(file);\n filesByRoot.set(root, group);\n }\n\n // Run linter for each project root\n const allFindings: ReviewFinding[] = [];\n const errors: string[] = [];\n\n for (const [projectRoot, groupFiles] of filesByRoot) {\n try {\n // Make file paths relative to project root\n // Input files might be relative to a different cwd (e.g., monorepo root)\n // We need them relative to the project root where we'll run the linter\n const relativeFiles = groupFiles.map(f => {\n // If the file path starts with the project root, strip it\n if (f.startsWith(projectRoot + '/')) {\n return f.slice(projectRoot.length + 1);\n }\n // Otherwise, try to make it relative\n return relative(projectRoot, f) || f;\n });\n\n const command = engine.buildCommand(relativeFiles, projectRoot);\n let output: string;\n\n try {\n output = execSync(command, {\n cwd: projectRoot,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n maxBuffer: this.maxBuffer,\n timeout: this.timeout,\n });\n } catch (execError: unknown) {\n // Most linters exit with non-zero when they find issues\n // Try to parse stdout anyway\n const err = execError as { stdout?: string; stderr?: string; message?: string };\n if (err.stdout) {\n output = err.stdout;\n } else {\n // Real error, not just lint findings\n errors.push(`${projectRoot}: ${err.message ?? 'Unknown error'}`);\n continue;\n }\n }\n\n // Parse output\n if (output && output.trim()) {\n try {\n const findings = engine.parseOutput(output, projectRoot);\n allFindings.push(...findings);\n } catch (parseError: unknown) {\n const err = parseError as { message?: string };\n errors.push(`${projectRoot}: Failed to parse output: ${err.message ?? 'Unknown error'}`);\n }\n }\n } catch (error: unknown) {\n const err = error as { message?: string };\n errors.push(`${projectRoot}: ${err.message ?? 'Unknown error'}`);\n }\n }\n\n return {\n engineId,\n findings: allFindings,\n files: relevantFiles,\n durationMs: Date.now() - startTime,\n error: errors.length > 0 ? errors.join('; ') : undefined,\n };\n }\n\n /**\n * Run all applicable linters on files.\n */\n async runAll(files: string[]): Promise<LinterResult[]> {\n const results: LinterResult[] = [];\n\n // Find which engines have relevant files\n const enginesWithFiles = new Set<string>();\n for (const file of files) {\n const ext = extname(file);\n for (const engine of this.engines.values()) {\n if (engine.extensions.includes(ext)) {\n enginesWithFiles.add(engine.id);\n }\n }\n }\n\n // Run each engine\n for (const engineId of enginesWithFiles) {\n // eslint-disable-next-line no-await-in-loop -- Sequential engine execution for resource control\n const result = await this.runEngine(engineId, files);\n results.push(result);\n }\n\n return results;\n }\n\n /**\n * Get all findings from multiple engine results.\n */\n static collectFindings(results: LinterResult[]): ReviewFinding[] {\n return results.flatMap(r => r.findings);\n }\n}\n\n/**\n * Quick helper to run all linters on files.\n */\nexport async function runLinters(files: string[]): Promise<ReviewFinding[]> {\n const runner = new LinterRunner();\n const results = await runner.runAll(files);\n return LinterRunner.collectFindings(results);\n}\n\n/**\n * Quick helper to run a specific linter.\n */\nexport async function runLinter(engineId: string, files: string[]): Promise<ReviewFinding[]> {\n const runner = new LinterRunner();\n const result = await runner.runEngine(engineId, files);\n return result.findings;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/review-heuristic",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"clean": "rimraf dist",
|
|
14
|
+
"dev": "tsup --config tsup.config.ts --watch",
|
|
15
|
+
"lint": "eslint src --ext .ts",
|
|
16
|
+
"lint:fix": "eslint . --fix",
|
|
17
|
+
"type-check": "tsc --noEmit",
|
|
18
|
+
"test": "vitest run --passWithNoTests",
|
|
19
|
+
"test:watch": "vitest"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@kb-labs/review-contracts": "^0.5.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",
|
|
26
|
+
"@types/node": "^24.3.3",
|
|
27
|
+
"tsup": "^8.5.0",
|
|
28
|
+
"typescript": "^5.6.3",
|
|
29
|
+
"rimraf": "^6.0.1",
|
|
30
|
+
"vitest": "^3.2.4"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20.0.0",
|
|
34
|
+
"pnpm": ">=9.0.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
]
|
|
39
|
+
}
|