/** * @fileoverview Schema Hashing System for Cross-Repo Parity Validation * Generates deterministic SHA-256 hashes for Zod schemas to detect schema drift * Part of CON-06: Cross-Repo Schema Parity & Integration Validation */ import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import { z } from 'zod'; /** * Generate a deterministic SHA-256 hash for a Zod schema * Uses the schema's internal definition (_def) for consistent hashing */ export function generateSchemaHash(schema: z.ZodTypeAny): string { try { // Use the schema's internal definition which is stable across environments const schemaDef = JSON.stringify(schema._def, Object.keys(schema._def).sort()); return crypto .createHash('sha256') .update(schemaDef) .digest('hex'); } catch (error) { throw new Error(`Failed to generate hash for schema: ${error}`); } } /** * Generate hashes for all exported schemas from the schemas index * Returns a map of schema name -> hash for comparison across repositories */ export function generateAllSchemaHashes(): Record { // Import all schemas dynamically to avoid circular dependencies const schemaModules = [ '../schemas/portfolio.js', '../schemas/market.js', '../schemas/ml.js', '../schemas/trading.js', '../schemas/helpers.js', '../schemas/auth.js', '../schemas/jobs.js', '../schemas/endpoint-meta.js', '../schemas/deprecation.js', '../schemas/pagination.js', '../schemas/streaming.js', '../schemas/marketplace.js', ]; const hashes: Record = {}; // We need to dynamically import and analyze each module // This is a simplified approach - in a real implementation, // we'd need to use a build-time analysis or reflection schemaModules.forEach(modulePath => { try { // For now, we'll use a placeholder hash for each module // In a full implementation, this would analyze the actual exported schemas const moduleName = modulePath.split('/').pop()?.replace('.js', '') || 'unknown'; hashes[moduleName] = crypto .createHash('sha256') .update(modulePath) .digest('hex') .substring(0, 16); // Short hash for demo } catch (error) { console.warn(`Warning: Could not process schema module ${modulePath}:`, error); } }); return hashes; } /** * Compare schema hashes between internal and external sources * Returns detailed diff information for parity validation */ export interface SchemaHashComparison { internalOnly: string[]; externalOnly: string[]; hashMismatches: Array<{ schemaName: string; internalHash: string; externalHash: string; }>; totalSchemas: number; parityScore: number; // 0-100 percentage } export function compareSchemaHashes( internalHashes: Record, externalHashes: Record ): SchemaHashComparison { const internalSchemas = Object.keys(internalHashes); const externalSchemas = Object.keys(externalHashes); const internalOnly = internalSchemas.filter(schema => !externalSchemas.includes(schema)); const externalOnly = externalSchemas.filter(schema => !internalSchemas.includes(schema)); const hashMismatches = internalSchemas .filter(schema => externalSchemas.includes(schema)) .filter(schema => internalHashes[schema] !== externalHashes[schema]) .map(schema => ({ schemaName: schema, internalHash: internalHashes[schema], externalHash: externalHashes[schema], })); const totalSchemas = Math.max(internalSchemas.length, externalSchemas.length); const matchingSchemas = totalSchemas - internalOnly.length - externalOnly.length - hashMismatches.length; const parityScore = totalSchemas > 0 ? (matchingSchemas / totalSchemas) * 100 : 100; return { internalOnly, externalOnly, hashMismatches, totalSchemas, parityScore, }; } /** * Save schema hashes to file for CI/CD artifact storage */ export function saveSchemaHashes(hashes: Record, outputPath: string): void { // Ensure directory exists const dir = path.dirname(outputPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(outputPath, JSON.stringify(hashes, null, 2)); } /** * Load schema hashes from file for comparison */ export function loadSchemaHashes(inputPath: string): Record { if (!fs.existsSync(inputPath)) { throw new Error(`Schema hashes file not found: ${inputPath}`); } return JSON.parse(fs.readFileSync(inputPath, 'utf8')); }