Skip to content

Commit 067bd88

Browse files
committed
feat: implement L3 cross-file context window for AI-generated multi-file contradictions
- Add CrossFileContextAnalyzer for analyzing file relationships and grouping - Enhance AI pipeline to support multi-file LLM analysis in L3 mode - Detect type mismatches, enum inconsistencies, signature contradictions across files - Add test suite for cross-file detection capabilities - Create demo showing before/after with cross-file type inconsistencies - Update types to support cross-file stage and detector category - Add configuration options for file grouping parameters This is the only AI audit tool that can detect cross-file contradictions that traditional tools (SonarQube, ESLint) miss because they analyze files in isolation. Related to OCR product roadmap Long-term priority item #8
1 parent eb64395 commit 067bd88

6 files changed

Lines changed: 795 additions & 7 deletions

File tree

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
/**
2+
* Cross-File Context Window for L3 AI Analysis
3+
*
4+
* Implements multi-file analysis to detect AI-generated contradictions
5+
* that span across multiple files, such as:
6+
* - Type mismatches between files
7+
* - Enum value inconsistencies
8+
* - Function signature mismatches
9+
* - Data format contradictions
10+
*
11+
* @since 0.5.0
12+
*/
13+
14+
import type { CodeUnit, SupportedLanguage, SymbolDef } from '../../ir/types.js';
15+
import type { DetectorResult } from '../../detectors/v4/types.js';
16+
17+
/**
18+
* File relationship information for grouping related files
19+
*/
20+
export interface FileRelationship {
21+
/** File path */
22+
file: string;
23+
/** Files that import this file */
24+
importedBy: string[];
25+
/** Files imported by this file */
26+
imports: string[];
27+
/** Files that share common dependencies (import the same modules) */
28+
coDependent: string[];
29+
/** File category (entry point, library, test, config, etc.) */
30+
category: 'entry' | 'library' | 'test' | 'config' | 'unknown';
31+
}
32+
33+
/**
34+
* Cross-file issue detected by multi-file analysis
35+
*/
36+
export interface CrossFileIssue {
37+
/** Type of cross-file contradiction */
38+
type: 'type-mismatch' | 'enum-inconsistency' | 'signature-mismatch' | 'data-format-contradiction' | 'import-cycle';
39+
/** Files involved in the contradiction */
40+
files: string[];
41+
/** Specific symbols/types/values that are inconsistent */
42+
symbols: string[];
43+
/** Detailed description of the issue */
44+
description: string;
45+
/** Severity level */
46+
severity: 'error' | 'warning' | 'info';
47+
/** Estimated confidence in the detection */
48+
confidence: number;
49+
}
50+
51+
/**
52+
* File group for multi-file LLM analysis
53+
*/
54+
export interface FileGroup {
55+
/** Unique ID for this group */
56+
id: string;
57+
/** Files in this group */
58+
files: CodeUnit[];
59+
/** Relationship type */
60+
relationship: 'import-chain' | 'shared-dependencies' | 'circular-imports' | 'cohesive-module';
61+
/** Primary file (entry point of the analysis) */
62+
primaryFile: CodeUnit;
63+
}
64+
65+
/**
66+
* Cross-file context analyzer
67+
*/
68+
export class CrossFileContextAnalyzer {
69+
private readonly maxFilesPerGroup: number;
70+
private readonly minGroupSize: number;
71+
72+
constructor(
73+
private config: {
74+
maxFilesPerGroup?: number;
75+
minGroupSize?: number;
76+
similarityThreshold?: number;
77+
} = {}
78+
) {
79+
this.maxFilesPerGroup = config.maxFilesPerGroup ?? 5;
80+
this.minGroupSize = config.minGroupSize ?? 2;
81+
}
82+
83+
/**
84+
* Analyze import relationships and group files for multi-file analysis
85+
*/
86+
analyzeFileRelationships(units: CodeUnit[]): FileRelationship[] {
87+
const fileMap = new Map<string, CodeUnit>();
88+
const relationships: FileRelationship[] = [];
89+
90+
// Create file map
91+
for (const unit of units) {
92+
if (unit.kind === 'file') {
93+
fileMap.set(unit.file, unit);
94+
}
95+
}
96+
97+
// Build relationships for each file
98+
for (const [filePath, unit] of fileMap) {
99+
const importedBy: string[] = [];
100+
const imports: string[] = [];
101+
const coDependent: string[] = [];
102+
103+
// Find which files import this file
104+
for (const [otherPath, otherUnit] of fileMap) {
105+
if (otherPath === filePath) continue;
106+
107+
// Check if other file imports this file
108+
const hasImport = otherUnit.imports.some(imp => {
109+
// Handle relative imports
110+
if (imp.isRelative) {
111+
// Simple relative import resolution (could be enhanced)
112+
const otherDir = otherPath.substring(0, otherPath.lastIndexOf('/'));
113+
const targetPath = `${otherDir}/${imp.module}`;
114+
return targetPath === filePath;
115+
}
116+
// For absolute imports, check if this is the main module
117+
return imp.module.startsWith(unit.file.split('.')[0]);
118+
});
119+
120+
if (hasImport) {
121+
importedBy.push(otherPath);
122+
}
123+
124+
// Find co-dependent files (files that import similar modules)
125+
const otherImports = otherUnit.imports.map(imp => imp.module);
126+
const commonImports = unit.imports
127+
.map(imp => imp.module)
128+
.filter(module => otherImports.includes(module));
129+
130+
if (commonImports.length >= 2) {
131+
coDependent.push(otherPath);
132+
}
133+
}
134+
135+
// Add the file's own imports
136+
for (const imp of unit.imports) {
137+
if (!imp.isRelative) {
138+
imports.push(imp.module);
139+
}
140+
}
141+
142+
// Determine category
143+
const category = this.categorizeFile(unit, filePath);
144+
145+
relationships.push({
146+
file: filePath,
147+
importedBy,
148+
imports,
149+
coDependent: [...new Set(coDependent)], // Remove duplicates
150+
category,
151+
});
152+
}
153+
154+
return relationships;
155+
}
156+
157+
/**
158+
* Group files for multi-file LLM analysis based on relationships
159+
*/
160+
groupFilesForAnalysis(units: CodeUnit[], relationships: FileRelationship[]): FileGroup[] {
161+
const groups: FileGroup[] = [];
162+
const processedFiles = new Set<string>();
163+
164+
// Find entry points (files imported by others, or main source files)
165+
const entryPoints = relationships
166+
.filter(rel => rel.category === 'entry' || rel.importedBy.length > 0)
167+
.sort((a, b) => b.importedBy.length - a.importedBy.length);
168+
169+
// Process each entry point
170+
for (const relationship of entryPoints) {
171+
if (processedFiles.has(relationship.file)) continue;
172+
173+
// Build group around this entry point
174+
const group = this.buildFileGroup(relationship, units, relationships, processedFiles);
175+
if (group && group.files.length >= this.minGroupSize) {
176+
groups.push(group);
177+
}
178+
}
179+
180+
// Add remaining files as single-file groups
181+
for (const unit of units) {
182+
if (unit.kind === 'file' && !processedFiles.has(unit.file)) {
183+
groups.push({
184+
id: `single-${unit.file.replace(/[^a-zA-Z0-9]/g, '-')}`,
185+
files: [unit],
186+
relationship: 'cohesive-module',
187+
primaryFile: unit,
188+
});
189+
}
190+
}
191+
192+
return groups;
193+
}
194+
195+
/**
196+
* Build a file group starting from an entry point
197+
*/
198+
private buildFileGroup(
199+
startRelationship: FileRelationship,
200+
allUnits: CodeUnit[],
201+
relationships: FileRelationship[],
202+
processedFiles: Set<string>
203+
): FileGroup | null {
204+
const groupFiles: CodeUnit[] = [];
205+
const importChain: string[] = [];
206+
let currentFile = startRelationship.file;
207+
208+
// Traverse import chain
209+
while (currentFile && groupFiles.length < this.maxFilesPerGroup) {
210+
if (processedFiles.has(currentFile)) break;
211+
212+
const unit = allUnits.find(u => u.file === currentFile && u.kind === 'file');
213+
if (!unit) break;
214+
215+
groupFiles.push(unit);
216+
importChain.push(currentFile);
217+
processedFiles.add(currentFile);
218+
219+
// Find next file in the chain (most imported file)
220+
const nextFile = this.findNextInChain(currentFile, relationships);
221+
if (!nextFile || processedFiles.has(nextFile)) break;
222+
223+
currentFile = nextFile;
224+
}
225+
226+
if (groupFiles.length < this.minGroupSize) {
227+
// Rollback processed files if group is too small
228+
for (const file of importChain) {
229+
processedFiles.delete(file);
230+
}
231+
return null;
232+
}
233+
234+
return {
235+
id: `group-${importChain.join('-').replace(/[^a-zA-Z0-9]/g, '-')}`,
236+
files: groupFiles,
237+
relationship: 'import-chain',
238+
primaryFile: groupFiles[0],
239+
};
240+
}
241+
242+
/**
243+
* Find the next file in the import chain
244+
*/
245+
private findNextInChain(currentFile: string, relationships: FileRelationship[]): string | null {
246+
const relationship = relationships.find(r => r.file === currentFile);
247+
if (!relationship) return null;
248+
249+
// Find the most imported file that's not already processed
250+
const nextFiles = relationship.importedBy
251+
.filter(file => relationships.find(r => r.file === file)?.category !== 'test');
252+
253+
if (nextFiles.length === 0) return null;
254+
255+
// Prefer entry points or files with many importers
256+
return nextFiles.reduce((best, file) => {
257+
const fileRel = relationships.find(r => r.file === file);
258+
const bestRel = relationships.find(r => r.file === best);
259+
return (fileRel?.importedBy.length || 0) > (bestRel?.importedBy.length || 0) ? file : best;
260+
});
261+
}
262+
263+
/**
264+
* Categorize a file based on its characteristics
265+
*/
266+
private categorizeFile(unit: CodeUnit, filePath: string): 'entry' | 'library' | 'test' | 'config' | 'unknown' {
267+
const fileName = filePath.toLowerCase();
268+
269+
// Entry points: main, index, app, server, etc.
270+
if (fileName.includes('main') || fileName.includes('index') || fileName.includes('app') ||
271+
fileName.includes('server') || fileName.includes('client')) {
272+
return 'entry';
273+
}
274+
275+
// Test files
276+
if (fileName.includes('test') || fileName.includes('spec') || fileName.includes('__tests__')) {
277+
return 'test';
278+
}
279+
280+
// Config files
281+
if (fileName.includes('config') || fileName.includes('env') || fileName.includes('setting')) {
282+
return 'config';
283+
}
284+
285+
// Library/utility files
286+
if (fileName.includes('util') || fileName.includes('helper') || fileName.includes('lib') ||
287+
fileName.includes('service') || fileName.includes('controller') || fileName.includes('model')) {
288+
return 'library';
289+
}
290+
291+
return 'unknown';
292+
}
293+
294+
/**
295+
* Extract cross-file issues from LLM responses
296+
*/
297+
extractCrossFileIssues(llmResponse: string, fileGroup: FileGroup): DetectorResult[] {
298+
const issues: DetectorResult[] = [];
299+
300+
try {
301+
// Parse LLM response (assuming JSON format)
302+
const response = JSON.parse(llmResponse);
303+
304+
if (response.issues && Array.isArray(response.issues)) {
305+
for (const issue of response.issues) {
306+
if (issue.crossFile && issue.files && issue.files.length > 1) {
307+
issues.push({
308+
detectorId: 'cross-file-context',
309+
severity: issue.severity || 'warning',
310+
category: 'cross-file-contradiction',
311+
messageKey: 'ai.cross-file.detected',
312+
message: issue.message || `Cross-file issue detected: ${issue.type}`,
313+
file: fileGroup.primaryFile.file,
314+
line: issue.line || 1,
315+
confidence: issue.confidence || 0.7,
316+
metadata: {
317+
crossFile: true,
318+
files: issue.files,
319+
issueType: issue.type,
320+
symbols: issue.symbols || [],
321+
},
322+
});
323+
}
324+
}
325+
}
326+
} catch (error) {
327+
// If LLM response is not valid JSON, skip cross-file extraction
328+
console.warn('Failed to parse LLM response for cross-file analysis:', error);
329+
}
330+
331+
return issues;
332+
}
333+
334+
/**
335+
* Build a multi-file prompt for LLM analysis
336+
*/
337+
buildMultiFilePrompt(fileGroup: FileGroup): string {
338+
const { files, primaryFile } = fileGroup;
339+
340+
let prompt = `You are a cross-file code analyzer specializing in detecting AI-generated contradictions across multiple files.
341+
342+
**File Group**: ${fileGroup.id}
343+
**Relationship**: ${fileGroup.relationship}
344+
**Files Analyzed**: ${files.length}
345+
346+
${files.map((file, index) => `
347+
**File ${index + 1}**: ${file.file}
348+
**Language**: ${file.language}
349+
**Code**:
350+
\`\`\`${file.language}
351+
${file.source.slice(0, 2000)}
352+
\`\`\`
353+
`).join('\n')}
354+
355+
**Cross-File Analysis Tasks**:
356+
1. **Type Inconsistencies**: Check if the same type/interface has different definitions across files
357+
2. **Enum Value Mismatches**: Look for enum values used inconsistently (e.g., Status.ACTIVE vs Status.active)
358+
3. **Function Signature Contradictions**: Verify function calls match their definitions across files
359+
4. **Data Format Conflicts**: Check if data structures are passed in incompatible formats
360+
5. **Import Path Issues**: Detect circular imports or missing imports
361+
6. **API Contract Violations**: Ensure function calls, class instantiations, and property access match actual definitions
362+
363+
**Focus on AI-Specific Issues**:
364+
- AI often generates inconsistent type definitions across files
365+
- AI may use different naming conventions for the same concept
366+
- AI sometimes creates function signatures that don't match their usage
367+
- AI might import modules but not use them correctly
368+
- AI can create data structures that are incompatible with their usage
369+
370+
**Respond in JSON format**:
371+
{
372+
"issues": [
373+
{
374+
"line": <line_number>,
375+
"severity": "error|warning|info",
376+
"message": "<detailed description of the cross-file issue>",
377+
"category": "type-mismatch|enum-inconsistency|signature-mismatch|data-format-contradiction|import-cycle",
378+
"crossFile": true,
379+
"files": ["<file1>", "<file2>", ...],
380+
"symbols": ["<symbol1>", "<symbol2>", ...]
381+
}
382+
]
383+
}
384+
385+
If no cross-file issues found, respond with: { "issues": [] }`;
386+
387+
return prompt;
388+
}
389+
}

0 commit comments

Comments
 (0)