@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 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
@@ -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
+ }