Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions VALIDATION_IMPLEMENTATION_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Validation Service Implementation Notes

## Issue Resolution

This document explains the implementation that resolves the visibility issue with the validation section in PR #1075.

## Problem
The validation section was not visible in the Publications tab because the validation service methods were stub implementations that returned empty results.

## Solution Implemented

### 1. ValidationContext.getFileContent()
- Now fetches actual file content from GitHub using githubService
- Implements caching for performance
- Throws appropriate errors when repository context is not set

### 2. ValidationContext.listFiles()
- Recursively lists all files in the repository
- Supports basic glob pattern matching
- Uses GitHub API to traverse directory structure

### 3. DAKArtifactValidationService.validateRepository()
- Complete orchestration of repository validation
- Lists all validatable files (bpmn, dmn, xml, json, yaml, fsh)
- Automatically determines component type based on file path
- Fetches each file and runs appropriate validation rules
- Returns comprehensive validation report with errors, warnings, and info

### 4. Type Updates
- Added `isValid` field to DAKValidationReport interface
- Updated all places that return DAKValidationReport to include isValid

## Result
The validation section in the Publications tab is now fully functional. When users:
1. Navigate to Publications tab
2. Select a component filter (or "All Components")
3. Click "Run Validation"

The system will:
1. Fetch all relevant files from the GitHub repository
2. Run validation rules on each file
3. Display summary results
4. Allow detailed viewing in a modal

## Files Modified
- `src/services/validation/ValidationContext.ts`
- `src/services/validation/DAKArtifactValidationService.ts`
- `src/services/validation/types.ts`
136 changes: 122 additions & 14 deletions src/services/validation/DAKArtifactValidationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,21 +226,127 @@ export class DAKArtifactValidationService {
// Set repository context
this.context.setRepositoryContext({ owner, repo, branch });

// TODO: Integrate with githubService to list all files
// For now, return empty report as placeholder
const fileResults: FileValidationResult[] = [];

// Calculate summary
const summary = this.calculateSummary(fileResults);
try {
// List all files in the repository
const allFiles = await this.context.listFiles('**/*');

// Filter files to only those we can validate
const validatableFiles = allFiles.filter(path => {
const ext = path.split('.').pop()?.toLowerCase();
return ext && ['bpmn', 'dmn', 'xml', 'json', 'yaml', 'yml', 'fsh'].includes(ext);
});

// Determine component and fileType for each file
const filesToValidate = validatableFiles.map(path => {
const ext = path.split('.').pop()?.toLowerCase() || '';
let component = 'general';
let fileType = ext;

// Determine component based on path
if (path.includes('/input/') || path.includes('\\input\\')) {
if (path.includes('/vocabulary/') || ext === 'fsh') {
component = 'terminology';
fileType = 'fsh';
} else if (path.includes('/profiles/')) {
component = 'fhir-profiles';
} else if (path.includes('/extensions/')) {
component = 'fhir-extensions';
}
} else if (path.includes('/business-processes/') || ext === 'bpmn') {
component = 'business-processes';
fileType = 'bpmn';
} else if (path.includes('/decision-logic/') || ext === 'dmn') {
component = 'decision-logic';
fileType = 'dmn';
}

// Check if we should skip this file based on component filter
if (options?.component && component !== options.component) {
return null;
}

return { path, fileType, component };
}).filter((file): file is { path: string; fileType: string; component: string } => file !== null);

// Fetch and validate files
const fileResults: FileValidationResult[] = [];

for (const file of filesToValidate) {
try {
const content = await this.context.getFileContent(file.path);
const result = await this.validateFile(file.path, content, file.fileType, file.component);
fileResults.push(result);
} catch (error) {
console.error(`Error validating file ${file.path}:`, error);
// Add error as validation result
fileResults.push({
filePath: file.path,
fileType: file.fileType,
component: file.component,
violations: [{
ruleCode: 'FILE-ACCESS-ERROR',
level: 'error',
message: `Failed to access file: ${error instanceof Error ? error.message : String(error)}`,
filePath: file.path
}],
isValid: false,
errorCount: 1,
warningCount: 0,
infoCount: 0,
timestamp: new Date()
});
}
}

return {
repository: { owner, repo, branch },
timestamp: new Date(),
summary,
fileResults,
canSave: summary.filesWithErrors === 0,
duration: Date.now() - startTime
};
// Calculate summary
const summary = this.calculateSummary(fileResults);
const isValid = summary.filesWithErrors === 0;

return {
repository: { owner, repo, branch },
timestamp: new Date(),
summary,
fileResults,
isValid,
canSave: summary.filesWithErrors === 0,
duration: Date.now() - startTime
};
} catch (error) {
console.error('Error validating repository:', error);
// Return error report
return {
repository: { owner, repo, branch },
timestamp: new Date(),
summary: {
totalFiles: 0,
validFiles: 0,
filesWithErrors: 1,
filesWithWarnings: 0,
totalErrors: 1,
totalWarnings: 0,
totalInfo: 0
},
fileResults: [{
filePath: 'repository',
fileType: 'unknown',
component: 'general',
violations: [{
ruleCode: 'REPOSITORY-ACCESS-ERROR',
level: 'error',
message: `Failed to access repository: ${error instanceof Error ? error.message : String(error)}`,
filePath: 'repository'
}],
isValid: false,
errorCount: 1,
warningCount: 0,
infoCount: 0,
timestamp: new Date()
}],
isValid: false,
canSave: false,
duration: Date.now() - startTime
};
}
}

/**
Expand Down Expand Up @@ -292,6 +398,7 @@ export class DAKArtifactValidationService {

// Calculate summary
const summary = this.calculateSummary(fileResults);
const isValid = summary.filesWithErrors === 0;

return {
repository: {
Expand All @@ -302,6 +409,7 @@ export class DAKArtifactValidationService {
timestamp: new Date(),
summary,
fileResults,
isValid,
canSave: summary.filesWithErrors === 0,
duration: Date.now() - startTime
};
Expand Down
84 changes: 75 additions & 9 deletions src/services/validation/ValidationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import { ValidationContext as IValidationContext } from './types';
import githubService from '../githubService';

/**
* Validation Context Implementation
Expand Down Expand Up @@ -71,12 +72,24 @@ export class ValidationContext implements IValidationContext {
return this.fileContentCache.get(filePath)!;
}

// In a real implementation, this would fetch from GitHub or staging ground
// For now, return empty string as placeholder
// TODO: Integrate with githubService and stagingGroundService
const content = '';
this.fileContentCache.set(filePath, content);
return content;
// Fetch from GitHub if repository context is available
if (this.repositoryContext) {
try {
const content = await githubService.getFileContent(
this.repositoryContext.owner,
this.repositoryContext.repo,
filePath,
this.repositoryContext.branch
);
this.fileContentCache.set(filePath, content);
return content;
} catch (error) {
console.error(`Error fetching file ${filePath}:`, error);
throw new Error(`Failed to fetch file: ${filePath}`);
}
}

throw new Error('No repository context set');
}

/**
Expand All @@ -86,9 +99,62 @@ export class ValidationContext implements IValidationContext {
* @returns Array of file paths
*/
async listFiles(pattern: string): Promise<string[]> {
// TODO: Integrate with githubService and stagingGroundService
// to list files matching the pattern
return [];
if (!this.repositoryContext) {
throw new Error('No repository context set');
}

try {
// For now, do a simple implementation that lists all files recursively
// In production, this should support actual glob patterns
const allFiles: string[] = [];
await this.listFilesRecursive('', allFiles);

// Simple pattern matching - convert glob to regex
// This is a basic implementation, in production use a proper glob library
if (pattern && pattern !== '**/*') {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`);
return allFiles.filter(file => regex.test(file));
}

return allFiles;
} catch (error) {
console.error('Error listing files:', error);
return [];
}
}

/**
* Recursively list all files in a directory
*
* @param path - Directory path
* @param allFiles - Accumulator for file paths
*/
private async listFilesRecursive(path: string, allFiles: string[]): Promise<void> {
if (!this.repositoryContext) return;

try {
const contents = await githubService.getDirectoryContents(
this.repositoryContext.owner,
this.repositoryContext.repo,
path,
this.repositoryContext.branch
);

for (const item of contents) {
if (item.type === 'file') {
allFiles.push(item.path);
} else if (item.type === 'dir') {
// Recursively list subdirectory
await this.listFilesRecursive(item.path, allFiles);
}
}
} catch (error) {
console.error(`Error listing directory ${path}:`, error);
}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/services/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ export interface DAKValidationReport {
/** Individual file results */
fileResults: FileValidationResult[];

/** Overall validation result - true if no errors found */
isValid: boolean;

/** Whether files can be saved (no error-level violations) */
canSave: boolean;

Expand Down