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 - z A - Z 0 - 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 - z A - Z 0 - 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