devibe
Version:
Intelligent repository cleanup with auto mode, AI learning, markdown consolidation, auto-consolidate workflow, context-aware classification, and cost optimization
676 lines ⢠28.3 kB
JavaScript
/**
* Rule Pack Validator
* Validates rule packs against the specification with clear, helpful error messages
*/
export class RulePackValidator {
/**
* Validate a rule pack with detailed, helpful error messages
*/
async validate(rulePack) {
const errors = [];
const warnings = [];
// Check if it's an object
if (!rulePack || typeof rulePack !== 'object') {
return {
valid: false,
errors: [{
path: '$',
message: 'Rule pack must be a valid object/JSON',
code: 'INVALID_TYPE'
}],
warnings: []
};
}
const pack = rulePack;
// ========================================================================
// Required Fields Validation
// ========================================================================
// 1. Schema version (REQUIRED)
if (!pack.schema) {
errors.push({
path: '$.schema',
message: 'Missing required field "schema". Expected: "devibe-rulepack/v1"',
code: 'MISSING_SCHEMA'
});
}
else if (pack.schema !== 'devibe-rulepack/v1') {
errors.push({
path: '$.schema',
message: `Invalid schema version "${pack.schema}". Expected: "devibe-rulepack/v1"`,
code: 'INVALID_SCHEMA_VERSION'
});
}
// 2. Metadata (REQUIRED)
if (!pack.metadata) {
errors.push({
path: '$.metadata',
message: 'Missing required field "metadata". This field contains name, version, author, etc.',
code: 'MISSING_METADATA'
});
}
else {
this.validateMetadata(pack.metadata, errors, warnings);
}
// ========================================================================
// Optional Fields Validation (with helpful guidance)
// ========================================================================
// 3. Extends (optional, but validate if present)
if (pack.extends !== undefined) {
this.validateExtends(pack.extends, errors, warnings);
}
// 4. Structure (optional)
if (pack.structure !== undefined) {
this.validateStructure(pack.structure, errors, warnings);
}
// 5. Test Organization (optional)
if (pack.testOrganization !== undefined) {
this.validateTestOrganization(pack.testOrganization, errors, warnings);
}
// 6. File Classification (optional)
if (pack.fileClassification !== undefined) {
this.validateFileClassification(pack.fileClassification, errors, warnings);
}
// 7. Technologies (optional)
if (pack.technologies !== undefined) {
this.validateTechnologies(pack.technologies, errors, warnings);
}
// 8. Monorepo (optional)
if (pack.monorepo !== undefined) {
this.validateMonorepo(pack.monorepo, errors, warnings);
}
// 9. Naming Conventions (optional)
if (pack.namingConventions !== undefined) {
this.validateNamingConventions(pack.namingConventions, errors, warnings);
}
// 10. Git (optional)
if (pack.git !== undefined) {
this.validateGit(pack.git, errors, warnings);
}
// 11. CI/CD (optional)
if (pack.cicd !== undefined) {
this.validateCICD(pack.cicd, errors, warnings);
}
// 12. Custom Rules (optional)
if (pack.customRules !== undefined) {
this.validateCustomRules(pack.customRules, errors, warnings);
}
// 13. Ignore (optional)
if (pack.ignore !== undefined) {
this.validateIgnore(pack.ignore, errors, warnings);
}
return {
valid: errors.length === 0,
errors,
warnings
};
}
// ========================================================================
// Metadata Validation
// ========================================================================
validateMetadata(metadata, errors, warnings) {
const path = '$.metadata';
// Required fields
if (!metadata.name) {
errors.push({
path: `${path}.name`,
message: 'Missing required field "name". Example: "@mycompany/rulepack-name"',
code: 'MISSING_NAME'
});
}
else if (typeof metadata.name !== 'string') {
errors.push({
path: `${path}.name`,
message: 'Field "name" must be a string',
code: 'INVALID_NAME_TYPE'
});
}
else if (!this.isValidPackageName(metadata.name)) {
warnings.push({
path: `${path}.name`,
message: 'Package name should follow npm scoping convention: "@org/name" or "name"',
code: 'INVALID_NAME_FORMAT'
});
}
if (!metadata.version) {
errors.push({
path: `${path}.version`,
message: 'Missing required field "version". Example: "1.0.0" (semver)',
code: 'MISSING_VERSION'
});
}
else if (!this.isValidSemver(metadata.version)) {
errors.push({
path: `${path}.version`,
message: `Invalid version "${metadata.version}". Must be valid semver (e.g., "1.0.0", "2.1.3")`,
code: 'INVALID_SEMVER'
});
}
if (!metadata.author) {
warnings.push({
path: `${path}.author`,
message: 'Recommended field "author" is missing. Helps users know who maintains this pack.',
code: 'MISSING_AUTHOR'
});
}
if (!metadata.description) {
warnings.push({
path: `${path}.description`,
message: 'Recommended field "description" is missing. Helps users understand this pack\'s purpose.',
code: 'MISSING_DESCRIPTION'
});
}
// Optional fields with guidance
if (metadata.tags && !Array.isArray(metadata.tags)) {
errors.push({
path: `${path}.tags`,
message: 'Field "tags" must be an array of strings. Example: ["nodejs", "typescript"]',
code: 'INVALID_TAGS_TYPE'
});
}
if (metadata.license && typeof metadata.license !== 'string') {
errors.push({
path: `${path}.license`,
message: 'Field "license" must be a string (SPDX identifier). Example: "MIT", "Apache-2.0"',
code: 'INVALID_LICENSE_TYPE'
});
}
if (metadata.homepage && !this.isValidUrl(metadata.homepage)) {
warnings.push({
path: `${path}.homepage`,
message: 'Field "homepage" should be a valid URL',
code: 'INVALID_HOMEPAGE_URL'
});
}
if (metadata.repository && !this.isValidUrl(metadata.repository)) {
warnings.push({
path: `${path}.repository`,
message: 'Field "repository" should be a valid URL',
code: 'INVALID_REPOSITORY_URL'
});
}
}
// ========================================================================
// Extends Validation
// ========================================================================
validateExtends(extends_, errors, warnings) {
const path = '$.extends';
if (!Array.isArray(extends_)) {
errors.push({
path,
message: 'Field "extends" must be an array of strings. Example: ["@devibe/nodejs-standard"]',
code: 'INVALID_EXTENDS_TYPE'
});
return;
}
extends_.forEach((item, index) => {
if (typeof item !== 'string') {
errors.push({
path: `${path}[${index}]`,
message: 'Each item in "extends" must be a string reference to another rule pack',
code: 'INVALID_EXTENDS_ITEM'
});
}
});
// Check for circular references (self-reference)
// Note: Full circular dependency check would require resolving all packs
}
// ========================================================================
// Structure Validation
// ========================================================================
validateStructure(structure, errors, warnings) {
const path = '$.structure';
if (structure.enforced !== undefined && typeof structure.enforced !== 'boolean') {
errors.push({
path: `${path}.enforced`,
message: 'Field "enforced" must be a boolean (true/false)',
code: 'INVALID_ENFORCED_TYPE'
});
}
if (structure.requiredFolders !== undefined) {
this.validateFolderRules(structure.requiredFolders, `${path}.requiredFolders`, errors, warnings);
}
if (structure.optionalFolders !== undefined) {
this.validateFolderRules(structure.optionalFolders, `${path}.optionalFolders`, errors, warnings);
}
if (structure.forbiddenAtRoot !== undefined) {
this.validateForbiddenPatterns(structure.forbiddenAtRoot, `${path}.forbiddenAtRoot`, errors, warnings);
}
}
validateFolderRules(folders, path, errors, warnings) {
if (!Array.isArray(folders)) {
errors.push({
path,
message: 'Folder rules must be an array. Example: [{ path: "src", description: "Source code" }]',
code: 'INVALID_FOLDER_RULES_TYPE'
});
return;
}
folders.forEach((folder, index) => {
const itemPath = `${path}[${index}]`;
if (!folder.path) {
errors.push({
path: `${itemPath}.path`,
message: 'Each folder rule must have a "path" field. Example: "src" or "tests/unit"',
code: 'MISSING_FOLDER_PATH'
});
}
if (!folder.description) {
warnings.push({
path: `${itemPath}.description`,
message: 'Recommended: Add a "description" to explain this folder\'s purpose',
code: 'MISSING_FOLDER_DESCRIPTION'
});
}
if (folder.allowedCategories && !Array.isArray(folder.allowedCategories)) {
errors.push({
path: `${itemPath}.allowedCategories`,
message: 'Field "allowedCategories" must be an array. Example: ["source", "test"]',
code: 'INVALID_ALLOWED_CATEGORIES'
});
}
});
}
validateForbiddenPatterns(patterns, path, errors, warnings) {
if (!Array.isArray(patterns)) {
errors.push({
path,
message: 'Forbidden patterns must be an array',
code: 'INVALID_FORBIDDEN_TYPE'
});
return;
}
patterns.forEach((pattern, index) => {
const itemPath = `${path}[${index}]`;
if (typeof pattern === 'string') {
// Simple string pattern is OK
return;
}
if (!pattern.message) {
warnings.push({
path: `${itemPath}.message`,
message: 'Recommended: Add a "message" to explain why this pattern is forbidden',
code: 'MISSING_FORBIDDEN_MESSAGE'
});
}
if (!pattern.pattern) {
errors.push({
path: `${itemPath}.pattern`,
message: 'Forbidden pattern must have a "pattern" field. Example: "*.test.ts"',
code: 'MISSING_FORBIDDEN_PATTERN'
});
}
});
}
// ========================================================================
// Test Organization Validation
// ========================================================================
validateTestOrganization(testOrg, errors, warnings) {
const path = '$.testOrganization';
if (testOrg.enabled !== undefined && typeof testOrg.enabled !== 'boolean') {
errors.push({
path: `${path}.enabled`,
message: 'Field "enabled" must be a boolean (true/false)',
code: 'INVALID_ENABLED_TYPE'
});
}
const validStrategies = ['separated', 'colocated', 'hybrid'];
if (testOrg.strategy && !validStrategies.includes(testOrg.strategy)) {
errors.push({
path: `${path}.strategy`,
message: `Invalid strategy "${testOrg.strategy}". Must be one of: ${validStrategies.join(', ')}`,
code: 'INVALID_STRATEGY'
});
}
if (testOrg.baseDirectory && typeof testOrg.baseDirectory !== 'string') {
errors.push({
path: `${path}.baseDirectory`,
message: 'Field "baseDirectory" must be a string. Example: "tests" or "test"',
code: 'INVALID_BASE_DIRECTORY'
});
}
if (testOrg.categories !== undefined) {
this.validateTestCategories(testOrg.categories, `${path}.categories`, errors, warnings);
}
}
validateTestCategories(categories, path, errors, warnings) {
if (!Array.isArray(categories)) {
errors.push({
path,
message: 'Test categories must be an array',
code: 'INVALID_CATEGORIES_TYPE'
});
return;
}
const validCategories = ['unit', 'integration', 'e2e', 'tdd', 'functional', 'performance', 'acceptance', 'contract'];
categories.forEach((category, index) => {
const itemPath = `${path}[${index}]`;
if (!category.name) {
errors.push({
path: `${itemPath}.name`,
message: `Missing "name" field. Valid categories: ${validCategories.join(', ')}`,
code: 'MISSING_CATEGORY_NAME'
});
}
else if (!validCategories.includes(category.name)) {
warnings.push({
path: `${itemPath}.name`,
message: `Uncommon category name "${category.name}". Standard categories: ${validCategories.join(', ')}`,
code: 'UNCOMMON_CATEGORY_NAME'
});
}
if (!category.patterns) {
errors.push({
path: `${itemPath}.patterns`,
message: 'Missing "patterns" array. Example: ["**/*.test.ts", "**/*.spec.ts"]',
code: 'MISSING_PATTERNS'
});
}
else if (!Array.isArray(category.patterns)) {
errors.push({
path: `${itemPath}.patterns`,
message: 'Field "patterns" must be an array of glob patterns',
code: 'INVALID_PATTERNS_TYPE'
});
}
if (!category.targetDirectory) {
errors.push({
path: `${itemPath}.targetDirectory`,
message: 'Missing "targetDirectory" field. Example: "tests/unit"',
code: 'MISSING_TARGET_DIRECTORY'
});
}
if (!category.description) {
warnings.push({
path: `${itemPath}.description`,
message: 'Recommended: Add a "description" to explain this test category',
code: 'MISSING_CATEGORY_DESCRIPTION'
});
}
});
}
// ========================================================================
// File Classification Validation
// ========================================================================
validateFileClassification(fileClass, errors, warnings) {
const path = '$.fileClassification';
if (!fileClass.categories) {
errors.push({
path: `${path}.categories`,
message: 'Missing "categories" object. Expected file category definitions.',
code: 'MISSING_FILE_CATEGORIES'
});
return;
}
const validCategories = ['source', 'config', 'documentation', 'script', 'test', 'asset'];
Object.entries(fileClass.categories).forEach(([name, definition]) => {
const categoryPath = `${path}.categories.${name}`;
if (!validCategories.includes(name)) {
warnings.push({
path: categoryPath,
message: `Custom category "${name}". Standard categories: ${validCategories.join(', ')}`,
code: 'CUSTOM_CATEGORY'
});
}
if (definition.extensions && !Array.isArray(definition.extensions)) {
errors.push({
path: `${categoryPath}.extensions`,
message: 'Field "extensions" must be an array. Example: [".ts", ".js"]',
code: 'INVALID_EXTENSIONS_TYPE'
});
}
if (definition.patterns && !Array.isArray(definition.patterns)) {
errors.push({
path: `${categoryPath}.patterns`,
message: 'Field "patterns" must be an array of glob patterns',
code: 'INVALID_PATTERNS_TYPE'
});
}
});
}
// ========================================================================
// Technologies Validation
// ========================================================================
validateTechnologies(technologies, errors, warnings) {
const path = '$.technologies';
if (typeof technologies !== 'object') {
errors.push({
path,
message: 'Field "technologies" must be an object mapping technology names to their rules',
code: 'INVALID_TECHNOLOGIES_TYPE'
});
return;
}
Object.entries(technologies).forEach(([techName, techDef]) => {
const techPath = `${path}.${techName}`;
if (!techDef.indicators) {
errors.push({
path: `${techPath}.indicators`,
message: 'Missing "indicators" array. Example: [{ file: "package.json", required: true }]',
code: 'MISSING_INDICATORS'
});
}
else if (!Array.isArray(techDef.indicators)) {
errors.push({
path: `${techPath}.indicators`,
message: 'Field "indicators" must be an array',
code: 'INVALID_INDICATORS_TYPE'
});
}
});
}
// ========================================================================
// Monorepo Validation
// ========================================================================
validateMonorepo(monorepo, errors, warnings) {
const path = '$.monorepo';
if (monorepo.enabled !== undefined && typeof monorepo.enabled !== 'boolean') {
errors.push({
path: `${path}.enabled`,
message: 'Field "enabled" must be a boolean (true/false)',
code: 'INVALID_ENABLED_TYPE'
});
}
const validStructures = ['nx', 'lerna', 'turborepo', 'pnpm-workspace', 'yarn-workspace', 'custom'];
if (monorepo.structure && !validStructures.includes(monorepo.structure)) {
errors.push({
path: `${path}.structure`,
message: `Invalid structure "${monorepo.structure}". Must be one of: ${validStructures.join(', ')}`,
code: 'INVALID_MONOREPO_STRUCTURE'
});
}
if (monorepo.packageRules && !Array.isArray(monorepo.packageRules)) {
errors.push({
path: `${path}.packageRules`,
message: 'Field "packageRules" must be an array',
code: 'INVALID_PACKAGE_RULES_TYPE'
});
}
}
// ========================================================================
// Naming Conventions Validation
// ========================================================================
validateNamingConventions(naming, errors, warnings) {
const path = '$.namingConventions';
const validStyles = ['camelCase', 'PascalCase', 'snake_case', 'kebab-case', 'SCREAMING_SNAKE_CASE'];
if (naming.files && !Array.isArray(naming.files)) {
errors.push({
path: `${path}.files`,
message: 'Field "files" must be an array',
code: 'INVALID_FILES_TYPE'
});
}
else if (naming.files) {
naming.files.forEach((rule, index) => {
if (!validStyles.includes(rule.convention)) {
errors.push({
path: `${path}.files[${index}].convention`,
message: `Invalid convention "${rule.convention}". Must be one of: ${validStyles.join(', ')}`,
code: 'INVALID_CONVENTION'
});
}
});
}
if (naming.folders && !Array.isArray(naming.folders)) {
errors.push({
path: `${path}.folders`,
message: 'Field "folders" must be an array',
code: 'INVALID_FOLDERS_TYPE'
});
}
}
// ========================================================================
// Git Validation
// ========================================================================
validateGit(git, errors, warnings) {
const path = '$.git';
if (git.requiredFiles && !Array.isArray(git.requiredFiles)) {
errors.push({
path: `${path}.requiredFiles`,
message: 'Field "requiredFiles" must be an array. Example: [".gitignore", "README.md"]',
code: 'INVALID_REQUIRED_FILES_TYPE'
});
}
if (git.suggestedIgnorePatterns && !Array.isArray(git.suggestedIgnorePatterns)) {
errors.push({
path: `${path}.suggestedIgnorePatterns`,
message: 'Field "suggestedIgnorePatterns" must be an array',
code: 'INVALID_IGNORE_PATTERNS_TYPE'
});
}
}
// ========================================================================
// CI/CD Validation
// ========================================================================
validateCICD(cicd, errors, warnings) {
const path = '$.cicd';
const validChecks = [
'secretScan',
'testOrganization',
'buildValidation',
'folderStructure',
'namingConventions',
'linting',
'testing'
];
if (cicd.preCommitChecks && !Array.isArray(cicd.preCommitChecks)) {
errors.push({
path: `${path}.preCommitChecks`,
message: 'Field "preCommitChecks" must be an array',
code: 'INVALID_PRE_COMMIT_CHECKS'
});
}
else if (cicd.preCommitChecks) {
cicd.preCommitChecks.forEach((check, index) => {
if (!validChecks.includes(check)) {
warnings.push({
path: `${path}.preCommitChecks[${index}]`,
message: `Unknown check "${check}". Valid checks: ${validChecks.join(', ')}`,
code: 'UNKNOWN_CHECK_TYPE'
});
}
});
}
}
// ========================================================================
// Custom Rules Validation
// ========================================================================
validateCustomRules(customRules, errors, warnings) {
const path = '$.customRules';
if (!Array.isArray(customRules)) {
errors.push({
path,
message: 'Field "customRules" must be an array',
code: 'INVALID_CUSTOM_RULES_TYPE'
});
return;
}
customRules.forEach((rule, index) => {
const rulePath = `${path}[${index}]`;
if (!rule.id) {
errors.push({
path: `${rulePath}.id`,
message: 'Custom rule must have an "id" field',
code: 'MISSING_RULE_ID'
});
}
if (!rule.description) {
warnings.push({
path: `${rulePath}.description`,
message: 'Recommended: Add a "description" to explain this custom rule',
code: 'MISSING_RULE_DESCRIPTION'
});
}
const validSeverities = ['error', 'warning', 'info'];
if (rule.severity && !validSeverities.includes(rule.severity)) {
errors.push({
path: `${rulePath}.severity`,
message: `Invalid severity "${rule.severity}". Must be one of: ${validSeverities.join(', ')}`,
code: 'INVALID_SEVERITY'
});
}
});
}
// ========================================================================
// Ignore Validation
// ========================================================================
validateIgnore(ignore, errors, warnings) {
const path = '$.ignore';
if (!Array.isArray(ignore)) {
errors.push({
path,
message: 'Field "ignore" must be an array of glob patterns. Example: ["node_modules/**", ".git/**"]',
code: 'INVALID_IGNORE_TYPE'
});
}
}
// ========================================================================
// Utility Validation Methods
// ========================================================================
isValidPackageName(name) {
// npm package name rules: lowercase, can have @scope/, hyphens, numbers
return /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
}
isValidSemver(version) {
// Basic semver validation (major.minor.patch)
return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/.test(version);
}
isValidUrl(url) {
try {
new URL(url);
return true;
}
catch {
return false;
}
}
}
/**
* Format validation result as human-readable output
*/
export function formatValidationResult(result) {
let output = '';
if (result.valid) {
output += 'â
Rule pack is valid!\n';
}
else {
output += `â Rule pack validation failed with ${result.errors.length} error(s)\n`;
}
if (result.errors.length > 0) {
output += '\nđ´ Errors:\n';
result.errors.forEach((error, index) => {
output += `\n${index + 1}. ${error.path}\n`;
output += ` ${error.message}\n`;
output += ` Code: ${error.code}\n`;
});
}
if (result.warnings.length > 0) {
output += '\nâ ď¸ Warnings:\n';
result.warnings.forEach((warning, index) => {
output += `\n${index + 1}. ${warning.path}\n`;
output += ` ${warning.message}\n`;
output += ` Code: ${warning.code}\n`;
});
}
return output;
}
//# sourceMappingURL=rulepack-validator.js.map