UNPKG

claude-code-templates

Version:

CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects

400 lines (342 loc) 10.9 kB
const BaseValidator = require('../BaseValidator'); const { execSync } = require('child_process'); const fs = require('fs-extra'); const path = require('path'); /** * ProvenanceValidator - Validates component provenance and metadata * * Checks: * - Author information * - Source repository tracking * - Git commit SHA tracking * - Timestamp tracking * - Version consistency * - Verified author status (future) */ class ProvenanceValidator extends BaseValidator { constructor() { super(); } /** * Validate component provenance * @param {object} component - Component data * @param {string} component.content - Raw markdown content * @param {string} component.path - File path * @param {object} options - Validation options * @param {boolean} options.requireGit - Require Git metadata * @param {boolean} options.requireAuthor - Require author information * @returns {Promise<object>} Validation results */ async validate(component, options = {}) { this.reset(); const { content, path: filePath } = component; const { requireGit = false, requireAuthor = false } = options; if (!content) { this.addError('PROV_E001', 'Component content is empty or missing', { path: filePath }); return this.getResults(); } // 1. Extract frontmatter metadata const metadata = this.extractMetadata(content, filePath); // 2. Validate author information this.validateAuthor(metadata, filePath, requireAuthor); // 3. Extract Git metadata if file exists if (fs.existsSync(filePath)) { const gitMetadata = await this.extractGitMetadata(filePath); if (gitMetadata) { this.validateGitMetadata(gitMetadata, filePath); } else if (requireGit) { this.addWarning('PROV_W001', 'No Git metadata found', { path: filePath }); } } else { this.addInfo('PROV_I001', 'File path does not exist (in-memory component)', { path: filePath }); } // 4. Validate repository URL if provided if (metadata.repository) { this.validateRepository(metadata.repository, filePath); } // 5. Validate version information if (metadata.version) { this.validateVersionConsistency(metadata.version, filePath); } // Add metadata to results const results = this.getResults(); results.metadata = { author: metadata.author || 'unknown', repository: metadata.repository || 'unknown', version: metadata.version || 'unversioned', extractedAt: new Date().toISOString() }; return results; } /** * Extract metadata from frontmatter * @param {string} content - Component content * @param {string} filePath - File path * @returns {object} Metadata object */ extractMetadata(content, filePath) { const metadata = {}; // Extract frontmatter const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!frontmatterMatch) { return metadata; } const frontmatter = frontmatterMatch[1]; // Extract author const authorMatch = frontmatter.match(/^author:\s*(.+)$/m); if (authorMatch) { metadata.author = authorMatch[1].trim().replace(/['"]/g, ''); } // Extract repository const repoMatch = frontmatter.match(/^repository:\s*(.+)$/m); if (repoMatch) { metadata.repository = repoMatch[1].trim().replace(/['"]/g, ''); } // Extract version const versionMatch = frontmatter.match(/^version:\s*(.+)$/m); if (versionMatch) { metadata.version = versionMatch[1].trim().replace(/['"]/g, ''); } return metadata; } /** * Validate author information */ validateAuthor(metadata, filePath, required) { if (!metadata.author) { if (required) { this.addError('PROV_E002', 'Author information is required but missing', { path: filePath }); } else { // Author is optional for components - metadata is stored in marketplace.json this.addInfo('PROV_I007', 'No author in component (metadata in marketplace.json)', { path: filePath }); } } else { this.addInfo('PROV_I002', `Author: ${metadata.author}`, { path: filePath }); // Validate author format (basic check) if (metadata.author.length < 2) { this.addWarning('PROV_W003', 'Author name seems too short', { path: filePath, author: metadata.author }); } } } /** * Extract Git metadata for a file * @param {string} filePath - Path to file * @returns {Promise<object|null>} Git metadata or null */ async extractGitMetadata(filePath) { try { // Get last commit SHA for this file const commitSha = execSync( `git log -1 --format=%H -- "${filePath}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ).trim(); if (!commitSha) { return null; } // Get commit author const author = execSync( `git log -1 --format=%an -- "${filePath}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ).trim(); // Get commit date const date = execSync( `git log -1 --format=%ai -- "${filePath}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ).trim(); // Get remote URL let remoteUrl = ''; try { remoteUrl = execSync( 'git config --get remote.origin.url', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ).trim(); } catch (e) { // No remote configured } return { commitSha, author, date, remoteUrl }; } catch (error) { // Not a git repository or file not tracked return null; } } /** * Validate Git metadata */ validateGitMetadata(gitMetadata, filePath) { const { commitSha, author, date, remoteUrl } = gitMetadata; this.addInfo('PROV_I003', `Git commit: ${commitSha.substring(0, 7)}`, { path: filePath, fullSha: commitSha, author, date }); if (remoteUrl) { this.addInfo('PROV_I004', `Repository: ${remoteUrl}`, { path: filePath, remoteUrl }); // Validate that remote URL is a recognized platform if (!this.isRecognizedGitPlatform(remoteUrl)) { this.addWarning('PROV_W004', 'Git remote is not a recognized platform', { path: filePath, remoteUrl, recognized: ['github.com', 'gitlab.com', 'bitbucket.org'] }); } } } /** * Validate repository URL */ validateRepository(repository, filePath) { // Check if it's a valid GitHub/GitLab/Bitbucket URL const gitPlatforms = [ 'github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org' ]; const isValid = gitPlatforms.some(platform => repository.includes(platform)); if (!isValid) { this.addWarning('PROV_W005', 'Repository URL is not from a recognized platform', { path: filePath, repository, recognized: gitPlatforms }); } else { this.addInfo('PROV_I005', `Repository: ${repository}`, { path: filePath }); } // Check for HTTPS if (repository.startsWith('http://')) { this.addWarning('PROV_W006', 'Repository URL uses HTTP (HTTPS recommended)', { path: filePath, repository }); } } /** * Validate version consistency */ validateVersionConsistency(version, filePath) { // Check if version follows semantic versioning const semverPattern = /^\d+\.\d+\.\d+(-[\w.]+)?$/; if (!semverPattern.test(version)) { this.addWarning('PROV_W007', 'Version does not follow semantic versioning', { path: filePath, version, expected: 'X.Y.Z or X.Y.Z-tag' }); } else { this.addInfo('PROV_I006', `Version: ${version}`, { path: filePath }); } } /** * Check if Git remote is a recognized platform */ isRecognizedGitPlatform(remoteUrl) { const platforms = [ 'github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org' ]; return platforms.some(platform => remoteUrl.includes(platform)); } /** * Generate provenance report * @param {object} component - Component to analyze * @returns {Promise<object>} Provenance report */ async generateProvenanceReport(component) { const result = await this.validate(component); // Extract Git metadata if available let gitMetadata = null; if (component.path && fs.existsSync(component.path)) { gitMetadata = await this.extractGitMetadata(component.path); } return { traceable: result.valid && result.metadata.author !== 'unknown', metadata: result.metadata, git: gitMetadata, issues: { errors: result.errors, warnings: result.warnings }, trustScore: this.calculateTrustScore(result, gitMetadata), timestamp: new Date().toISOString() }; } /** * Calculate trust score based on provenance data * @param {object} result - Validation results * @param {object} gitMetadata - Git metadata * @returns {number} Trust score (0-100) */ calculateTrustScore(result, gitMetadata) { let score = 50; // Base score // Has author: +15 if (result.metadata.author !== 'unknown') { score += 15; } // Has repository: +15 if (result.metadata.repository !== 'unknown') { score += 15; } // Has version: +10 if (result.metadata.version !== 'unversioned') { score += 10; } // Has Git metadata: +10 if (gitMetadata) { score += 10; } // Deduct for warnings and errors score -= result.warningCount * 2; score -= result.errorCount * 10; return Math.max(0, Math.min(100, score)); } /** * Batch validate provenance for multiple components * @param {Array<object>} components - Components to validate * @returns {Promise<object>} Batch validation results */ async batchValidate(components) { const results = { total: components.length, traceable: 0, untraceable: 0, averageTrustScore: 0, components: [] }; let totalTrustScore = 0; for (const component of components) { const report = await this.generateProvenanceReport(component); results.components.push({ path: component.path, traceable: report.traceable, trustScore: report.trustScore, author: report.metadata.author, repository: report.metadata.repository }); if (report.traceable) { results.traceable++; } else { results.untraceable++; } totalTrustScore += report.trustScore; } results.averageTrustScore = Math.round(totalTrustScore / components.length); return results; } } module.exports = ProvenanceValidator;