@rigour-labs/core 3.0.6 → 4.0.1
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/dist/deep/fact-extractor.d.ts +80 -0
- package/dist/deep/fact-extractor.js +626 -0
- package/dist/deep/fact-extractor.test.d.ts +1 -0
- package/dist/deep/fact-extractor.test.js +547 -0
- package/dist/deep/index.d.ts +14 -0
- package/dist/deep/index.js +12 -0
- package/dist/deep/prompts.d.ts +22 -0
- package/dist/deep/prompts.js +374 -0
- package/dist/deep/prompts.test.d.ts +1 -0
- package/dist/deep/prompts.test.js +220 -0
- package/dist/deep/verifier.d.ts +16 -0
- package/dist/deep/verifier.js +388 -0
- package/dist/deep/verifier.test.d.ts +1 -0
- package/dist/deep/verifier.test.js +514 -0
- package/dist/gates/deep-analysis.d.ts +28 -0
- package/dist/gates/deep-analysis.js +302 -0
- package/dist/gates/runner.d.ts +4 -2
- package/dist/gates/runner.js +46 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +12 -2
- package/dist/inference/cloud-provider.d.ts +34 -0
- package/dist/inference/cloud-provider.js +126 -0
- package/dist/inference/index.d.ts +17 -0
- package/dist/inference/index.js +23 -0
- package/dist/inference/model-manager.d.ts +26 -0
- package/dist/inference/model-manager.js +106 -0
- package/dist/inference/sidecar-provider.d.ts +15 -0
- package/dist/inference/sidecar-provider.js +153 -0
- package/dist/inference/types.d.ts +77 -0
- package/dist/inference/types.js +19 -0
- package/dist/settings.d.ts +104 -0
- package/dist/settings.js +186 -0
- package/dist/storage/db.d.ts +16 -0
- package/dist/storage/db.js +132 -0
- package/dist/storage/findings.d.ts +14 -0
- package/dist/storage/findings.js +38 -0
- package/dist/storage/index.d.ts +9 -0
- package/dist/storage/index.js +8 -0
- package/dist/storage/patterns.d.ts +35 -0
- package/dist/storage/patterns.js +62 -0
- package/dist/storage/scans.d.ts +42 -0
- package/dist/storage/scans.js +55 -0
- package/dist/templates/universal-config.js +19 -0
- package/dist/types/index.d.ts +438 -15
- package/dist/types/index.js +41 -1
- package/package.json +6 -2
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// We test the internal extractFileFacts by importing extractFacts and factsToPromptString
|
|
3
|
+
// But we need to test extraction logic directly, so we also test via the public API
|
|
4
|
+
const mockGlobby = vi.hoisted(() => vi.fn());
|
|
5
|
+
const mockReadFile = vi.hoisted(() => vi.fn());
|
|
6
|
+
vi.mock('globby', () => ({
|
|
7
|
+
globby: mockGlobby,
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('fs-extra', () => ({
|
|
10
|
+
default: {
|
|
11
|
+
readFile: mockReadFile,
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
import { extractFacts, factsToPromptString } from './fact-extractor.js';
|
|
15
|
+
describe('Fact Extractor', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
// ── TypeScript extraction ──
|
|
20
|
+
describe('TypeScript/JavaScript extraction', () => {
|
|
21
|
+
it('should extract classes with methods and dependencies', async () => {
|
|
22
|
+
mockGlobby.mockResolvedValue(['src/service.ts']);
|
|
23
|
+
mockReadFile.mockResolvedValue(`
|
|
24
|
+
import { Logger } from './logger';
|
|
25
|
+
|
|
26
|
+
export class UserService {
|
|
27
|
+
constructor(
|
|
28
|
+
private db: Database,
|
|
29
|
+
private logger: Logger
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
async findById(id: string): Promise<User> {
|
|
33
|
+
return this.db.find(id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async create(data: CreateUserDto): Promise<User> {
|
|
37
|
+
this.logger.info('Creating user');
|
|
38
|
+
return this.db.create(data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async update(id: string, data: UpdateUserDto): Promise<User> {
|
|
42
|
+
return this.db.update(id, data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async delete(id: string): Promise<void> {
|
|
46
|
+
return this.db.delete(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`);
|
|
50
|
+
const facts = await extractFacts('/project');
|
|
51
|
+
expect(facts).toHaveLength(1);
|
|
52
|
+
expect(facts[0].language).toBe('typescript');
|
|
53
|
+
expect(facts[0].classes).toHaveLength(1);
|
|
54
|
+
expect(facts[0].classes[0].name).toBe('UserService');
|
|
55
|
+
expect(facts[0].classes[0].methodCount).toBeGreaterThanOrEqual(4);
|
|
56
|
+
expect(facts[0].classes[0].dependencies).toContain('Database');
|
|
57
|
+
expect(facts[0].classes[0].dependencies).toContain('Logger');
|
|
58
|
+
});
|
|
59
|
+
it('should extract functions with params and async detection', async () => {
|
|
60
|
+
mockGlobby.mockResolvedValue(['src/utils.ts']);
|
|
61
|
+
mockReadFile.mockResolvedValue(`
|
|
62
|
+
export async function processData(input: string, config: Config, options: Options): Promise<Result> {
|
|
63
|
+
if (input.length > 0) {
|
|
64
|
+
if (config.validate) {
|
|
65
|
+
if (options.strict) {
|
|
66
|
+
return validate(input);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { data: input };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function simpleHelper(x: number): number {
|
|
74
|
+
return x * 2;
|
|
75
|
+
}
|
|
76
|
+
`);
|
|
77
|
+
const facts = await extractFacts('/project');
|
|
78
|
+
const funcs = facts[0].functions;
|
|
79
|
+
expect(funcs.length).toBeGreaterThanOrEqual(1);
|
|
80
|
+
const processData = funcs.find(f => f.name === 'processData');
|
|
81
|
+
expect(processData).toBeDefined();
|
|
82
|
+
expect(processData.isAsync).toBe(true);
|
|
83
|
+
expect(processData.isExported).toBe(true);
|
|
84
|
+
expect(processData.paramCount).toBe(3);
|
|
85
|
+
expect(processData.maxNesting).toBeGreaterThanOrEqual(3);
|
|
86
|
+
});
|
|
87
|
+
it('should extract imports from ES modules and require', async () => {
|
|
88
|
+
mockGlobby.mockResolvedValue(['src/index.ts']);
|
|
89
|
+
mockReadFile.mockResolvedValue(`
|
|
90
|
+
import { Router } from 'express';
|
|
91
|
+
import path from 'path';
|
|
92
|
+
const fs = require('fs-extra');
|
|
93
|
+
import type { Config } from './types';
|
|
94
|
+
|
|
95
|
+
export function init() {
|
|
96
|
+
const router = Router();
|
|
97
|
+
return router;
|
|
98
|
+
}
|
|
99
|
+
`);
|
|
100
|
+
const facts = await extractFacts('/project');
|
|
101
|
+
expect(facts[0].imports).toContain('express');
|
|
102
|
+
expect(facts[0].imports).toContain('path');
|
|
103
|
+
expect(facts[0].imports).toContain('fs-extra');
|
|
104
|
+
expect(facts[0].imports).toContain('./types');
|
|
105
|
+
});
|
|
106
|
+
it('should extract error handling patterns', async () => {
|
|
107
|
+
mockGlobby.mockResolvedValue(['src/api.ts']);
|
|
108
|
+
mockReadFile.mockResolvedValue(`
|
|
109
|
+
async function fetchData() {
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch('/api');
|
|
112
|
+
return res.json();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(err);
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function riskyOp() {
|
|
120
|
+
try {
|
|
121
|
+
await doSomething();
|
|
122
|
+
} catch (err) {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fetchData().then(data => {
|
|
127
|
+
process(data);
|
|
128
|
+
}).catch(() => {});
|
|
129
|
+
`);
|
|
130
|
+
const facts = await extractFacts('/project');
|
|
131
|
+
expect(facts[0].errorHandling.length).toBeGreaterThanOrEqual(2);
|
|
132
|
+
const tryCatches = facts[0].errorHandling.filter(e => e.type === 'try-catch');
|
|
133
|
+
expect(tryCatches.length).toBeGreaterThanOrEqual(2);
|
|
134
|
+
// Strategy detection finds console.error first → 'log'
|
|
135
|
+
const strategies = tryCatches.map(e => e.strategy);
|
|
136
|
+
expect(strategies.length).toBeGreaterThanOrEqual(2);
|
|
137
|
+
});
|
|
138
|
+
it('should detect test files and count assertions', async () => {
|
|
139
|
+
mockGlobby.mockResolvedValue(['src/utils.test.ts']);
|
|
140
|
+
mockReadFile.mockResolvedValue(`
|
|
141
|
+
import { describe, it, expect } from 'vitest';
|
|
142
|
+
|
|
143
|
+
describe('Utils', () => {
|
|
144
|
+
it('should add numbers', () => {
|
|
145
|
+
expect(add(1, 2)).toBe(3);
|
|
146
|
+
expect(add(0, 0)).toBe(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle negatives', () => {
|
|
150
|
+
expect(add(-1, 1)).toBe(0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
`);
|
|
154
|
+
const facts = await extractFacts('/project');
|
|
155
|
+
expect(facts[0].hasTests).toBe(true);
|
|
156
|
+
expect(facts[0].testAssertions).toBeGreaterThanOrEqual(3);
|
|
157
|
+
});
|
|
158
|
+
it('should extract exports', async () => {
|
|
159
|
+
mockGlobby.mockResolvedValue(['src/types.ts']);
|
|
160
|
+
mockReadFile.mockResolvedValue(`
|
|
161
|
+
export interface User {
|
|
162
|
+
id: string;
|
|
163
|
+
name: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const DEFAULT_CONFIG = {};
|
|
167
|
+
|
|
168
|
+
export function createUser(): User {
|
|
169
|
+
return { id: '1', name: 'test' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type Severity = 'high' | 'low';
|
|
173
|
+
`);
|
|
174
|
+
const facts = await extractFacts('/project');
|
|
175
|
+
expect(facts[0].exports).toContain('User');
|
|
176
|
+
expect(facts[0].exports).toContain('DEFAULT_CONFIG');
|
|
177
|
+
expect(facts[0].exports).toContain('createUser');
|
|
178
|
+
expect(facts[0].exports).toContain('Severity');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
// ── Python extraction ──
|
|
182
|
+
describe('Python extraction', () => {
|
|
183
|
+
it('should extract Python classes', async () => {
|
|
184
|
+
mockGlobby.mockResolvedValue(['app/service.py']);
|
|
185
|
+
mockReadFile.mockResolvedValue(`
|
|
186
|
+
class UserService:
|
|
187
|
+
def __init__(self, db):
|
|
188
|
+
self.db = db
|
|
189
|
+
|
|
190
|
+
def find_by_id(self, user_id):
|
|
191
|
+
return self.db.find(user_id)
|
|
192
|
+
|
|
193
|
+
def create(self, data):
|
|
194
|
+
return self.db.create(data)
|
|
195
|
+
|
|
196
|
+
def _validate(self, data):
|
|
197
|
+
pass
|
|
198
|
+
`);
|
|
199
|
+
const facts = await extractFacts('/project');
|
|
200
|
+
expect(facts[0].language).toBe('python');
|
|
201
|
+
expect(facts[0].classes).toHaveLength(1);
|
|
202
|
+
expect(facts[0].classes[0].name).toBe('UserService');
|
|
203
|
+
expect(facts[0].classes[0].methodCount).toBeGreaterThanOrEqual(3);
|
|
204
|
+
});
|
|
205
|
+
it('should extract Python imports', async () => {
|
|
206
|
+
mockGlobby.mockResolvedValue(['app/main.py']);
|
|
207
|
+
mockReadFile.mockResolvedValue(`
|
|
208
|
+
import os
|
|
209
|
+
from pathlib import Path
|
|
210
|
+
import json
|
|
211
|
+
from typing import Optional
|
|
212
|
+
from app.service import UserService
|
|
213
|
+
|
|
214
|
+
def main():
|
|
215
|
+
svc = UserService()
|
|
216
|
+
return svc
|
|
217
|
+
`);
|
|
218
|
+
const facts = await extractFacts('/project');
|
|
219
|
+
expect(facts[0].imports).toContain('os');
|
|
220
|
+
expect(facts[0].imports).toContain('pathlib');
|
|
221
|
+
expect(facts[0].imports).toContain('app.service');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
// ── Go extraction ──
|
|
225
|
+
describe('Go extraction', () => {
|
|
226
|
+
it('should extract Go structs with fields and methods', async () => {
|
|
227
|
+
mockGlobby.mockResolvedValue(['pkg/server.go']);
|
|
228
|
+
mockReadFile.mockResolvedValue(`package server
|
|
229
|
+
|
|
230
|
+
import (
|
|
231
|
+
"net/http"
|
|
232
|
+
"sync"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
type Server struct {
|
|
236
|
+
addr string
|
|
237
|
+
port int
|
|
238
|
+
handler http.Handler
|
|
239
|
+
mu sync.Mutex
|
|
240
|
+
*Base
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func NewServer(addr string, port int) *Server {
|
|
244
|
+
return &Server{addr: addr, port: port}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
func (s *Server) Start() error {
|
|
248
|
+
s.mu.Lock()
|
|
249
|
+
defer s.mu.Unlock()
|
|
250
|
+
return http.ListenAndServe(s.addr, s.handler)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func (s *Server) Stop() error {
|
|
254
|
+
return nil
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func (s *Server) Handler() http.Handler {
|
|
258
|
+
return s.handler
|
|
259
|
+
}
|
|
260
|
+
`);
|
|
261
|
+
const facts = await extractFacts('/project');
|
|
262
|
+
expect(facts[0].language).toBe('go');
|
|
263
|
+
expect(facts[0].structs).toBeDefined();
|
|
264
|
+
expect(facts[0].structs.length).toBeGreaterThanOrEqual(1);
|
|
265
|
+
const server = facts[0].structs[0];
|
|
266
|
+
expect(server.name).toBe('Server');
|
|
267
|
+
expect(server.fieldCount).toBeGreaterThanOrEqual(4); // addr, port, handler, mu, Base
|
|
268
|
+
expect(server.methodCount).toBeGreaterThanOrEqual(3); // Start, Stop, Handler
|
|
269
|
+
expect(server.methods).toContain('Start');
|
|
270
|
+
expect(server.methods).toContain('Stop');
|
|
271
|
+
expect(server.embeds).toContain('Base');
|
|
272
|
+
});
|
|
273
|
+
it('should extract Go interfaces', async () => {
|
|
274
|
+
mockGlobby.mockResolvedValue(['pkg/store.go']);
|
|
275
|
+
mockReadFile.mockResolvedValue(`package store
|
|
276
|
+
|
|
277
|
+
type Store interface {
|
|
278
|
+
Get(key string) (string, error)
|
|
279
|
+
Set(key string, value string) error
|
|
280
|
+
Delete(key string) error
|
|
281
|
+
List(prefix string) ([]string, error)
|
|
282
|
+
Close() error
|
|
283
|
+
Watch(key string) <-chan Event
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
type SimpleStore struct {
|
|
287
|
+
data map[string]string
|
|
288
|
+
}
|
|
289
|
+
`);
|
|
290
|
+
const facts = await extractFacts('/project');
|
|
291
|
+
expect(facts[0].interfaces).toBeDefined();
|
|
292
|
+
expect(facts[0].interfaces.length).toBeGreaterThanOrEqual(1);
|
|
293
|
+
const store = facts[0].interfaces[0];
|
|
294
|
+
expect(store.name).toBe('Store');
|
|
295
|
+
expect(store.methodCount).toBe(6);
|
|
296
|
+
expect(store.methods).toContain('Get');
|
|
297
|
+
expect(store.methods).toContain('Close');
|
|
298
|
+
});
|
|
299
|
+
it('should extract Go functions with receiver methods', async () => {
|
|
300
|
+
mockGlobby.mockResolvedValue(['pkg/handler.go']);
|
|
301
|
+
mockReadFile.mockResolvedValue(`package handler
|
|
302
|
+
|
|
303
|
+
type Handler struct {
|
|
304
|
+
service Service
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
func NewHandler(svc Service) *Handler {
|
|
308
|
+
return &Handler{service: svc}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
312
|
+
if r.Method == "GET" {
|
|
313
|
+
h.handleGet(w, r)
|
|
314
|
+
} else {
|
|
315
|
+
h.handlePost(w, r)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
func (h *Handler) handleGet(w http.ResponseWriter, r *http.Request) {
|
|
320
|
+
data, err := h.service.Find(r.URL.Query().Get("id"))
|
|
321
|
+
if err != nil {
|
|
322
|
+
http.Error(w, err.Error(), 500)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
json.NewEncoder(w).Encode(data)
|
|
326
|
+
}
|
|
327
|
+
`);
|
|
328
|
+
const facts = await extractFacts('/project');
|
|
329
|
+
const funcs = facts[0].functions;
|
|
330
|
+
// Should have receiver methods named as Receiver.Method
|
|
331
|
+
const serveHTTP = funcs.find(f => f.name === 'Handler.ServeHTTP');
|
|
332
|
+
expect(serveHTTP).toBeDefined();
|
|
333
|
+
const handleGet = funcs.find(f => f.name === 'Handler.handleGet');
|
|
334
|
+
expect(handleGet).toBeDefined();
|
|
335
|
+
// NewHandler should be a standalone func (no receiver)
|
|
336
|
+
const newHandler = funcs.find(f => f.name === 'NewHandler');
|
|
337
|
+
expect(newHandler).toBeDefined();
|
|
338
|
+
expect(newHandler.isExported).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
it('should count concurrency metrics', async () => {
|
|
341
|
+
mockGlobby.mockResolvedValue(['pkg/worker.go']);
|
|
342
|
+
mockReadFile.mockResolvedValue(`package worker
|
|
343
|
+
|
|
344
|
+
import "sync"
|
|
345
|
+
|
|
346
|
+
func StartWorkers(n int) {
|
|
347
|
+
var mu sync.Mutex
|
|
348
|
+
var wg sync.WaitGroup
|
|
349
|
+
ch := make(chan int, 10)
|
|
350
|
+
|
|
351
|
+
for i := 0; i < n; i++ {
|
|
352
|
+
wg.Add(1)
|
|
353
|
+
go processItem(ch, &mu, &wg)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
go monitorWorkers(ch)
|
|
357
|
+
|
|
358
|
+
defer close(ch)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func processItem(ch chan int, mu *sync.Mutex, wg *sync.WaitGroup) {
|
|
362
|
+
defer wg.Done()
|
|
363
|
+
for item := range ch {
|
|
364
|
+
mu.Lock()
|
|
365
|
+
process(item)
|
|
366
|
+
mu.Unlock()
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
`);
|
|
370
|
+
const facts = await extractFacts('/project');
|
|
371
|
+
expect(facts[0].goroutines).toBeGreaterThanOrEqual(2);
|
|
372
|
+
expect(facts[0].channels).toBeGreaterThanOrEqual(1);
|
|
373
|
+
expect(facts[0].defers).toBeGreaterThanOrEqual(2);
|
|
374
|
+
expect(facts[0].mutexes).toBeGreaterThanOrEqual(2);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
// ── Quality metrics ──
|
|
378
|
+
describe('Quality metrics', () => {
|
|
379
|
+
it('should detect magic numbers', async () => {
|
|
380
|
+
mockGlobby.mockResolvedValue(['src/calc.ts']);
|
|
381
|
+
mockReadFile.mockResolvedValue(`
|
|
382
|
+
export function calculate(input: number): number {
|
|
383
|
+
const base = input * 42;
|
|
384
|
+
const tax = base * 17;
|
|
385
|
+
const fee = 325 + 1250;
|
|
386
|
+
const limit = 9999;
|
|
387
|
+
return base + tax + fee + limit;
|
|
388
|
+
}
|
|
389
|
+
`);
|
|
390
|
+
const facts = await extractFacts('/project');
|
|
391
|
+
expect(facts[0].magicNumbers).toBeGreaterThanOrEqual(3);
|
|
392
|
+
});
|
|
393
|
+
it('should count TODOs', async () => {
|
|
394
|
+
mockGlobby.mockResolvedValue(['src/app.ts']);
|
|
395
|
+
mockReadFile.mockResolvedValue(`
|
|
396
|
+
// TODO: implement caching
|
|
397
|
+
export function getData() {
|
|
398
|
+
// FIXME: this is slow
|
|
399
|
+
const data = fetch('/api');
|
|
400
|
+
// HACK: workaround for bug
|
|
401
|
+
return data;
|
|
402
|
+
}
|
|
403
|
+
`);
|
|
404
|
+
const facts = await extractFacts('/project');
|
|
405
|
+
expect(facts[0].todoCount).toBe(4); // TODO, FIXME, HACK, WORKAROUND
|
|
406
|
+
});
|
|
407
|
+
it('should calculate comment ratio', async () => {
|
|
408
|
+
mockGlobby.mockResolvedValue(['src/app.ts']);
|
|
409
|
+
mockReadFile.mockResolvedValue(`
|
|
410
|
+
// This is a comment
|
|
411
|
+
// Another comment
|
|
412
|
+
export function foo() {
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function bar() {
|
|
417
|
+
return 2;
|
|
418
|
+
}
|
|
419
|
+
`);
|
|
420
|
+
const facts = await extractFacts('/project');
|
|
421
|
+
expect(facts[0].commentRatio).toBeGreaterThan(0);
|
|
422
|
+
});
|
|
423
|
+
it('should skip trivial files (< 3 lines)', async () => {
|
|
424
|
+
mockGlobby.mockResolvedValue(['src/empty.ts']);
|
|
425
|
+
mockReadFile.mockResolvedValue(`// empty\n`);
|
|
426
|
+
const facts = await extractFacts('/project');
|
|
427
|
+
expect(facts).toHaveLength(0);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
// ── factsToPromptString ──
|
|
431
|
+
describe('factsToPromptString', () => {
|
|
432
|
+
it('should serialize file facts to a prompt string', () => {
|
|
433
|
+
const facts = [
|
|
434
|
+
{
|
|
435
|
+
path: 'src/service.ts',
|
|
436
|
+
language: 'typescript',
|
|
437
|
+
lineCount: 150,
|
|
438
|
+
classes: [{
|
|
439
|
+
name: 'UserService',
|
|
440
|
+
lineStart: 5,
|
|
441
|
+
lineEnd: 140,
|
|
442
|
+
methodCount: 8,
|
|
443
|
+
methods: ['find', 'create', 'update', 'delete', 'validate', 'transform', 'cache', 'notify'],
|
|
444
|
+
publicMethods: ['find', 'create', 'update', 'delete'],
|
|
445
|
+
lineCount: 135,
|
|
446
|
+
dependencies: ['Database', 'Logger'],
|
|
447
|
+
}],
|
|
448
|
+
functions: [],
|
|
449
|
+
imports: ['express', './types', 'lodash'],
|
|
450
|
+
exports: ['UserService'],
|
|
451
|
+
errorHandling: [
|
|
452
|
+
{ type: 'try-catch', lineStart: 10, isEmpty: false, strategy: 'throw' },
|
|
453
|
+
{ type: 'try-catch', lineStart: 30, isEmpty: true, strategy: 'ignore' },
|
|
454
|
+
],
|
|
455
|
+
testAssertions: 0,
|
|
456
|
+
hasTests: false,
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
const result = factsToPromptString(facts);
|
|
460
|
+
expect(result).toContain('FILE: src/service.ts');
|
|
461
|
+
expect(result).toContain('CLASS UserService');
|
|
462
|
+
expect(result).toContain('135 lines');
|
|
463
|
+
expect(result).toContain('8 methods');
|
|
464
|
+
expect(result).toContain('deps: Database, Logger');
|
|
465
|
+
expect(result).toContain('ERROR_HANDLING');
|
|
466
|
+
expect(result).toContain('1 empty catches');
|
|
467
|
+
expect(result).toContain('IMPORTS: 3');
|
|
468
|
+
});
|
|
469
|
+
it('should include Go structs and concurrency info', () => {
|
|
470
|
+
const facts = [
|
|
471
|
+
{
|
|
472
|
+
path: 'pkg/worker.go',
|
|
473
|
+
language: 'go',
|
|
474
|
+
lineCount: 200,
|
|
475
|
+
classes: [],
|
|
476
|
+
functions: [{
|
|
477
|
+
name: 'StartWorkers',
|
|
478
|
+
lineStart: 10,
|
|
479
|
+
lineEnd: 60,
|
|
480
|
+
lineCount: 50,
|
|
481
|
+
paramCount: 2,
|
|
482
|
+
params: ['n int', 'config Config'],
|
|
483
|
+
maxNesting: 3,
|
|
484
|
+
hasReturn: true,
|
|
485
|
+
isAsync: true,
|
|
486
|
+
isExported: true,
|
|
487
|
+
}],
|
|
488
|
+
imports: ['sync', 'context'],
|
|
489
|
+
exports: [],
|
|
490
|
+
errorHandling: [],
|
|
491
|
+
testAssertions: 0,
|
|
492
|
+
hasTests: false,
|
|
493
|
+
structs: [{
|
|
494
|
+
name: 'Worker',
|
|
495
|
+
lineStart: 5,
|
|
496
|
+
lineEnd: 15,
|
|
497
|
+
fieldCount: 4,
|
|
498
|
+
methodCount: 3,
|
|
499
|
+
methods: ['Start', 'Stop', 'Process'],
|
|
500
|
+
lineCount: 10,
|
|
501
|
+
embeds: [],
|
|
502
|
+
}],
|
|
503
|
+
goroutines: 5,
|
|
504
|
+
channels: 2,
|
|
505
|
+
defers: 3,
|
|
506
|
+
mutexes: 1,
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
const result = factsToPromptString(facts);
|
|
510
|
+
expect(result).toContain('STRUCT Worker');
|
|
511
|
+
expect(result).toContain('4 fields');
|
|
512
|
+
expect(result).toContain('3 methods');
|
|
513
|
+
expect(result).toContain('CONCURRENCY');
|
|
514
|
+
expect(result).toContain('goroutines:5');
|
|
515
|
+
expect(result).toContain('channels:2');
|
|
516
|
+
expect(result).toContain('defers:3');
|
|
517
|
+
expect(result).toContain('mutexes:1');
|
|
518
|
+
});
|
|
519
|
+
it('should respect maxChars budget', () => {
|
|
520
|
+
const manyFacts = Array.from({ length: 100 }, (_, i) => ({
|
|
521
|
+
path: `src/file${i}.ts`,
|
|
522
|
+
language: 'typescript',
|
|
523
|
+
lineCount: 100,
|
|
524
|
+
classes: [],
|
|
525
|
+
functions: [{
|
|
526
|
+
name: `func${i}`,
|
|
527
|
+
lineStart: 1,
|
|
528
|
+
lineEnd: 50,
|
|
529
|
+
lineCount: 50,
|
|
530
|
+
paramCount: 2,
|
|
531
|
+
params: ['a', 'b'],
|
|
532
|
+
maxNesting: 2,
|
|
533
|
+
hasReturn: true,
|
|
534
|
+
isAsync: false,
|
|
535
|
+
isExported: true,
|
|
536
|
+
}],
|
|
537
|
+
imports: ['express', 'lodash', 'react'],
|
|
538
|
+
exports: [`func${i}`],
|
|
539
|
+
errorHandling: [],
|
|
540
|
+
testAssertions: 0,
|
|
541
|
+
hasTests: false,
|
|
542
|
+
}));
|
|
543
|
+
const result = factsToPromptString(manyFacts, 500);
|
|
544
|
+
expect(result.length).toBeLessThanOrEqual(600); // Allow some slack
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Analysis Pipeline — AST → LLM → Verify
|
|
3
|
+
*
|
|
4
|
+
* Step 1: AST extracts structured facts from code
|
|
5
|
+
* Step 2: LLM interprets facts and identifies quality issues
|
|
6
|
+
* Step 3: AST verifies LLM isn't hallucinating
|
|
7
|
+
*
|
|
8
|
+
* Neither AST nor LLM works alone. Together they're accurate.
|
|
9
|
+
*/
|
|
10
|
+
export { extractFacts, factsToPromptString } from './fact-extractor.js';
|
|
11
|
+
export type { FileFacts, ClassFact, FunctionFact, ErrorHandlingFact, StructFact, InterfaceFact } from './fact-extractor.js';
|
|
12
|
+
export { buildAnalysisPrompt, buildCrossFilePrompt, chunkFacts, DEEP_SYSTEM_PROMPT } from './prompts.js';
|
|
13
|
+
export { verifyFindings } from './verifier.js';
|
|
14
|
+
export type { VerifiedFinding } from './verifier.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Analysis Pipeline — AST → LLM → Verify
|
|
3
|
+
*
|
|
4
|
+
* Step 1: AST extracts structured facts from code
|
|
5
|
+
* Step 2: LLM interprets facts and identifies quality issues
|
|
6
|
+
* Step 3: AST verifies LLM isn't hallucinating
|
|
7
|
+
*
|
|
8
|
+
* Neither AST nor LLM works alone. Together they're accurate.
|
|
9
|
+
*/
|
|
10
|
+
export { extractFacts, factsToPromptString } from './fact-extractor.js';
|
|
11
|
+
export { buildAnalysisPrompt, buildCrossFilePrompt, chunkFacts, DEEP_SYSTEM_PROMPT } from './prompts.js';
|
|
12
|
+
export { verifyFindings } from './verifier.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Engineering — Step 2 of the three-step pipeline.
|
|
3
|
+
* Constructs structured prompts that ask the LLM to interpret AST-extracted facts.
|
|
4
|
+
*/
|
|
5
|
+
import type { FileFacts } from './fact-extractor.js';
|
|
6
|
+
/**
|
|
7
|
+
* System prompt that defines the LLM's role and output format.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEEP_SYSTEM_PROMPT = "You are an expert code reviewer and software architect performing deep quality analysis. You receive AST-extracted facts about a codebase and must identify quality issues, anti-patterns, and best practice violations.\n\nIMPORTANT RULES:\n1. ONLY report issues you can verify from the provided facts. Do NOT hallucinate files, classes, or functions.\n2. Every finding MUST reference a real file and entity from the facts.\n3. Be specific: include file paths, struct/class names, function names, line counts.\n4. Assign confidence scores honestly: 0.9+ only for certain issues, 0.5-0.7 for probable issues.\n5. Respond ONLY with valid JSON matching the schema below. No explanation text outside JSON.\n6. AIM for 5-15 findings per batch. Be thorough \u2014 report ALL issues you can identify, not just the most obvious ones.\n7. For Go code: treat structs as classes, receiver methods as class methods. Check Go idioms specifically.\n\nOUTPUT SCHEMA:\n{\n \"findings\": [\n {\n \"category\": \"string (see CATEGORIES below)\",\n \"severity\": \"string (critical|high|medium|low|info)\",\n \"file\": \"string (exact file path from facts)\",\n \"line\": \"number or null\",\n \"description\": \"string (what the issue is, referencing specific entities)\",\n \"suggestion\": \"string (actionable fix recommendation)\",\n \"confidence\": \"number 0.0-1.0\"\n }\n ]\n}\n\nCATEGORIES:\n SOLID Principles:\n srp_violation - Single file/struct/class handles multiple unrelated responsibilities\n ocp_violation - Code requires modification (not extension) for new behavior\n lsp_violation - Subtypes break substitutability contracts\n isp_violation - Interface has too many methods forcing unnecessary implementations\n dip_violation - High-level modules depend directly on low-level implementations\n\n Design Patterns & Anti-patterns:\n god_class - Class/struct with too many fields, methods, or responsibilities (>8 methods or >300 lines)\n god_function - Function exceeding 50 lines or doing too many things\n feature_envy - Function/method uses another module's data more than its own\n shotgun_surgery - A single change requires modifying many files\n long_params - Function with 4+ parameters (use struct/options pattern)\n data_clump - Same group of fields/params repeated across multiple structs/functions\n inappropriate_intimacy - Two modules too tightly coupled, accessing each other's internals\n primitive_obsession - Using primitives instead of domain types (string for email, int for ID)\n lazy_class - Struct/class that does too little to justify its existence\n speculative_generality - Over-engineered abstractions not justified by current usage\n refused_bequest - Subtype/implementation ignores inherited behavior\n\n DRY & Duplication:\n dry_violation - Duplicated logic across files that should be extracted\n copy_paste_code - Nearly identical functions/methods in different files\n\n Error Handling:\n error_inconsistency - Mixed error handling strategies in same package/module\n empty_catch - Empty catch/except blocks that silently swallow errors\n error_swallowing - Errors logged but not propagated when they should be\n missing_error_check - Return values (especially errors) not checked\n panic_in_library - Library code using panic/os.Exit instead of returning errors\n\n Concurrency (Go/Rust/async languages):\n race_condition - Shared mutable state accessed without synchronization\n goroutine_leak - Goroutines spawned without cancellation/context mechanism\n missing_context - Functions that should accept context.Context but don't\n channel_misuse - Unbuffered channels that could deadlock, or missing close()\n mutex_scope - Mutex held too long or across I/O operations\n\n Testing:\n test_quality - Insufficient assertions, no edge cases, weak coverage\n test_coupling - Tests tightly coupled to implementation details\n missing_test - Complex public function/method with no corresponding test\n test_duplication - Multiple tests verifying the same behavior redundantly\n\n Architecture:\n architecture - Layer violations, wrong dependency direction\n circular_dependency - Modules that import each other\n package_cohesion - Package/directory contains unrelated concerns\n api_design - Exported API is confusing, inconsistent, or poorly structured\n missing_abstraction - Direct usage where an interface/abstraction would improve design\n\n Language Idioms:\n language_idiom - Language-specific anti-patterns\n naming_convention - Names don't follow language conventions (Go: MixedCaps, Python: snake_case)\n dead_code - Unreferenced exports, unused functions\n magic_number - Numeric literals without named constants\n\n Performance & Security:\n performance - Obvious performance anti-patterns (N+1 queries, unbounded allocations)\n resource_leak - Opened resources (files, connections, readers) not properly closed\n hardcoded_config - Configuration values hardcoded instead of externalized\n\n Code Smells:\n code_smell - General smell with refactoring suggestion\n complex_conditional - Deeply nested or overly complex conditional logic\n long_file - File exceeds reasonable length for its responsibility";
|
|
10
|
+
/**
|
|
11
|
+
* Build the analysis prompt for a batch of file facts.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildAnalysisPrompt(factsStr: string, checks?: Record<string, boolean>): string;
|
|
14
|
+
/**
|
|
15
|
+
* Build a cross-file analysis prompt that looks at patterns across the whole codebase.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildCrossFilePrompt(allFacts: FileFacts[]): string;
|
|
18
|
+
/**
|
|
19
|
+
* Chunk file facts into batches that fit within token limits.
|
|
20
|
+
* Groups related files (same directory) together.
|
|
21
|
+
*/
|
|
22
|
+
export declare function chunkFacts(facts: FileFacts[], maxCharsPerChunk?: number): FileFacts[][];
|