Inspection Consistency
Overview
This document explains how the MCP JetBrains Code Inspections server ensures consistent, deterministic results across different runs, environments, and configurations.
Consistency Principles
1. Deterministic Execution
Every inspection run with the same inputs should produce identical outputs:
// From InspectionExecutor.ts
async inspect(params: InspectionParams): Promise<InspectionResult> {
// Always use the same execution flow
const ide = await this.ideSelector.select();
const profile = await this.profileManager.resolve(params);
const command = this.strategy.buildCommand(params);
const result = await this.execute(command);
return this.processor.process(result);
}
2. Isolated Execution Environment
Each inspection runs in an isolated environment:
// From InspectionStrategy.ts
private createIsolatedEnvironment(): EnvironmentConfig {
return {
// Separate config directory to avoid conflicts
configPath: path.join(os.tmpdir(), 'idea-config', process.pid.toString()),
// Separate system directory for caches
systemPath: path.join(os.tmpdir(), 'idea-system', process.pid.toString()),
// Consistent JVM options
jvmOptions: [
'-Xmx2048m',
'-XX:+UseG1GC',
'-Djava.awt.headless=true'
]
};
}
3. Stable Output Ordering
Results are consistently ordered:
// From ResultParser.ts
private sortDiagnostics(diagnostics: Diagnostic[]): Diagnostic[] {
return diagnostics.sort((a, b) => {
// First by file path
const fileCompare = a.file.localeCompare(b.file);
if (fileCompare !== 0) return fileCompare;
// Then by line number
const lineCompare = a.line - b.line;
if (lineCompare !== 0) return lineCompare;
// Finally by column
return (a.column || 0) - (b.column || 0);
});
}
Configuration Management
Environment Variable Consistency
All configuration is managed through environment variables:
# Consistent configuration across runs
export FORCE_INSPECT_PATH="/Applications/WebStorm.app/Contents/bin/inspect.sh"
export FORCE_PROJECT_ROOT="/path/to/project"
export FORCE_PROFILE_PATH="/path/to/profile.xml"
export INSPECTION_TIMEOUT="120000"
export EXCLUDE_INSPECTIONS="SpellCheckingInspection"
export RESPONSE_FORMAT="json"
Profile Management
The InspectionProfileManager
ensures consistent profile usage:
export class InspectionProfileManager {
private profileCache = new Map<string, string>();
async resolveProfile(options: ProfileOptions): Promise<string> {
const cacheKey = this.getCacheKey(options);
// Return cached profile for consistency
if (this.profileCache.has(cacheKey)) {
return this.profileCache.get(cacheKey)!;
}
const profile = await this.findProfile(options);
this.profileCache.set(cacheKey, profile);
return profile;
}
}
Handling Non-Deterministic Elements
1. Timestamps
Remove or normalize timestamps from output:
class DiagnosticNormalizer {
normalize(diagnostic: RawDiagnostic): NormalizedDiagnostic {
// Remove timestamp fields
const { timestamp, ...rest } = diagnostic;
return {
...rest,
// Use stable identifiers
id: this.generateStableId(rest),
};
}
private generateStableId(diagnostic: any): string {
// Create deterministic ID from content
const content = `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}:${diagnostic.inspection}`;
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 8);
}
}
2. File System Paths
Normalize all paths to be relative:
class PathNormalizer {
normalize(absolutePath: string, projectRoot: string): string {
// Always use forward slashes
const normalized = absolutePath.replace(/\\/g, '/');
// Make relative to project root
if (normalized.startsWith(projectRoot)) {
return path.relative(projectRoot, normalized).replace(/\\/g, '/');
}
return normalized;
}
}
3. Random Inspection Order
Force consistent inspection execution order:
class InspectionOrdering {
orderInspections(inspections: Inspection[]): Inspection[] {
// Sort by inspection ID for consistency
return inspections.sort((a, b) => a.id.localeCompare(b.id));
}
}
Version Compatibility
IDE Version Management
Track and handle IDE version differences:
interface IDEVersion {
major: number;
minor: number;
patch: number;
build: string;
}
class VersionCompatibility {
ensureCompatibility(ide: IDE): void {
const version = this.parseVersion(ide.version);
// Warn about version differences
if (this.hasBreakingChanges(version)) {
this.logger.warn(`IDE version ${ide.version} may produce different results`);
}
// Apply version-specific adjustments
this.applyVersionAdjustments(version);
}
}
Profile Version Compatibility
Handle profile format changes:
<!-- Profile with version information -->
<profile version="1.0">
<option name="myName" value="Unified" />
<option name="PROFILE_VERSION" value="2023.3" />
<!-- Version-specific settings -->
<inspection_tool class="ModernInspection" enabled="true" level="WARNING">
<!-- Only in 2023.3+ -->
<option name="requiresVersion" value="2023.3" />
</inspection_tool>
</profile>
Caching Strategy
Result Caching
Implement deterministic caching:
class InspectionCache {
private cache = new Map<string, CachedResult>();
getCacheKey(params: InspectionParams): string {
// Create deterministic cache key
const elements = [
params.targetPath,
params.profilePath || 'default',
params.filter?.only?.sort().join(',') || '',
params.filter?.exclude?.sort().join(',') || '',
this.getFileHash(params.targetPath),
];
return crypto.createHash('sha256').update(elements.join('|')).digest('hex');
}
private getFileHash(targetPath: string): string {
// Hash file contents for cache invalidation
const content = fs.readFileSync(targetPath, 'utf8');
return crypto.createHash('md5').update(content).digest('hex');
}
}
Cache Invalidation
Properly invalidate caches:
class CacheInvalidator {
shouldInvalidate(cached: CachedResult, current: FileInfo): boolean {
return (
// File modified
cached.fileHash !== current.hash ||
// Profile changed
cached.profileHash !== current.profileHash ||
// IDE version changed
cached.ideVersion !== current.ideVersion ||
// Cache expired
Date.now() - cached.timestamp > this.maxAge
);
}
}
Testing Consistency
Regression Testing
Ensure consistent results across changes:
describe('Inspection Consistency', () => {
it('should produce identical results for multiple runs', async () => {
const params = {
targetPath: 'test/fixtures/sample.ts',
profilePath: 'test/fixtures/profile.xml',
};
const result1 = await executor.inspect(params);
const result2 = await executor.inspect(params);
expect(result1).toEqual(result2);
});
it('should maintain consistency across IDE restarts', async () => {
const result1 = await executor.inspect(params);
// Simulate IDE restart
await executor.restart();
const result2 = await executor.inspect(params);
expect(normalizeResults(result1)).toEqual(normalizeResults(result2));
});
});
Consistency Metrics
Track consistency over time:
class ConsistencyMonitor {
async checkConsistency(params: InspectionParams): Promise<ConsistencyReport> {
const results: InspectionResult[] = [];
// Run multiple times
for (let i = 0; i < 5; i++) {
results.push(await this.executor.inspect(params));
}
return {
consistent: this.areResultsIdentical(results),
variance: this.calculateVariance(results),
differences: this.findDifferences(results),
};
}
}
Common Consistency Issues
1. Timing-Dependent Results
Problem: Some inspections depend on timing or external resources.
Solution:
class TimingNormalizer {
normalize(diagnostic: Diagnostic): Diagnostic {
// Remove timing-sensitive information
if (diagnostic.message.includes('took')) {
diagnostic.message = diagnostic.message.replace(/took \d+ms/, 'took Xms');
}
return diagnostic;
}
}
2. Plugin Loading Order
Problem: Plugins may load in different orders.
Solution:
class PluginManager {
async loadPlugins(): Promise<Plugin[]> {
const plugins = await this.discoverPlugins();
// Always load in alphabetical order
return plugins.sort((a, b) => a.id.localeCompare(b.id));
}
}
3. Locale Differences
Problem: Different system locales affect messages.
Solution:
# Force consistent locale
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
Best Practices for Consistency
1. Use Explicit Configuration
Always specify configuration explicitly:
{
"env": {
"FORCE_INSPECT_PATH": "/path/to/inspect.sh",
"FORCE_PROJECT_ROOT": "/path/to/project",
"FORCE_PROFILE_PATH": "/path/to/profile.xml",
"INSPECTION_TIMEOUT": "120000"
}
}
2. Version Lock
Lock IDE and plugin versions:
{
"requirements": {
"ide": "WebStorm 2023.3.2",
"plugins": {
"eslint": "2.4.0",
"prettier": "1.2.3"
}
}
}
3. Isolated Execution
Run inspections in isolated environments:
# Use Docker for complete isolation
docker run -v $(pwd):/project \
mcp-jetbrains-inspections \
inspect /project/src
4. Result Verification
Implement result verification:
class ResultVerifier {
verify(result: InspectionResult): boolean {
return (
this.hasExpectedStructure(result) && this.hasValidSeverities(result) && this.hasConsistentPaths(result) && this.hasNoDuplicates(result)
);
}
}
Monitoring and Debugging
Consistency Logging
Enable detailed logging for debugging:
class ConsistencyLogger {
logInspectionRun(params: InspectionParams, result: InspectionResult): void {
const entry = {
timestamp: new Date().toISOString(),
params: this.sanitizeParams(params),
result: {
totalProblems: result.totalProblems,
checksum: this.calculateChecksum(result),
},
environment: {
ide: process.env.FORCE_INSPECT_PATH,
profile: process.env.FORCE_PROFILE_PATH,
nodeVersion: process.version,
},
};
this.logger.info('Inspection run', entry);
}
}
Diff Analysis
Analyze differences between runs:
class DiffAnalyzer {
analyze(result1: InspectionResult, result2: InspectionResult): DiffReport {
return {
added: this.findAdded(result1, result2),
removed: this.findRemoved(result1, result2),
changed: this.findChanged(result1, result2),
summary: this.generateSummary(result1, result2),
};
}
}
Performance vs Consistency Trade-offs
Caching Strategies
Balance performance with consistency:
enum CacheStrategy {
NONE = 'none', // Always fresh, slowest
CONSERVATIVE = 'conservative', // Cache with strict invalidation
AGGRESSIVE = 'aggressive', // Cache aggressively, fastest
}
class CacheManager {
constructor(private strategy: CacheStrategy) {}
async get(key: string): Promise<InspectionResult | null> {
switch (this.strategy) {
case CacheStrategy.NONE:
return null;
case CacheStrategy.CONSERVATIVE:
return this.getIfValid(key);
case CacheStrategy.AGGRESSIVE:
return this.getRegardless(key);
}
}
}
Parallel Execution
Maintain consistency with parallel processing:
class ParallelExecutor {
async inspectParallel(files: string[]): Promise<InspectionResult[]> {
// Sort files for consistent ordering
const sortedFiles = files.sort();
// Process in deterministic batches
const batchSize = 4;
const results: InspectionResult[] = [];
for (let i = 0; i < sortedFiles.length; i += batchSize) {
const batch = sortedFiles.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map((file) => this.inspect(file)));
results.push(...batchResults);
}
return results;
}
}
Future Improvements
Planned Enhancements
- Checksum Verification: Verify result integrity with checksums
- Reproducible Builds: Support fully reproducible inspection runs
- Consistency Testing: Automated consistency regression tests
- Drift Detection: Detect when results start to drift
Research Areas
- Machine Learning: Detect and correct inconsistencies automatically
- Distributed Inspection: Consistent results across distributed systems
- Version Migration: Automatic migration between IDE versions