UNPKG

@fission-ai/openspec

Version:

AI-native system for spec-driven development

108 lines 3.57 kB
import * as fs from 'node:fs'; import { parse as parseYaml } from 'yaml'; import { SchemaYamlSchema } from './types.js'; export class SchemaValidationError extends Error { constructor(message) { super(message); this.name = 'SchemaValidationError'; } } /** * Loads and validates an artifact schema from a YAML file. */ export function loadSchema(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); return parseSchema(content); } /** * Parses and validates an artifact schema from YAML content. */ export function parseSchema(yamlContent) { const parsed = parseYaml(yamlContent); // Validate with Zod const result = SchemaYamlSchema.safeParse(parsed); if (!result.success) { const errors = result.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); throw new SchemaValidationError(`Invalid schema: ${errors}`); } const schema = result.data; // Check for duplicate artifact IDs validateNoDuplicateIds(schema.artifacts); // Check that all requires references are valid validateRequiresReferences(schema.artifacts); // Check for cycles validateNoCycles(schema.artifacts); return schema; } /** * Validates that there are no duplicate artifact IDs. */ function validateNoDuplicateIds(artifacts) { const seen = new Set(); for (const artifact of artifacts) { if (seen.has(artifact.id)) { throw new SchemaValidationError(`Duplicate artifact ID: ${artifact.id}`); } seen.add(artifact.id); } } /** * Validates that all `requires` references point to valid artifact IDs. */ function validateRequiresReferences(artifacts) { const validIds = new Set(artifacts.map(a => a.id)); for (const artifact of artifacts) { for (const req of artifact.requires) { if (!validIds.has(req)) { throw new SchemaValidationError(`Invalid dependency reference in artifact '${artifact.id}': '${req}' does not exist`); } } } } /** * Validates that there are no cyclic dependencies. * Uses DFS to detect cycles and reports the full cycle path. */ function validateNoCycles(artifacts) { const artifactMap = new Map(artifacts.map(a => [a.id, a])); const visited = new Set(); const inStack = new Set(); const parent = new Map(); function dfs(id) { visited.add(id); inStack.add(id); const artifact = artifactMap.get(id); if (!artifact) return null; for (const dep of artifact.requires) { if (!visited.has(dep)) { parent.set(dep, id); const cycle = dfs(dep); if (cycle) return cycle; } else if (inStack.has(dep)) { // Found a cycle - reconstruct the path const cyclePath = [dep]; let current = id; while (current !== dep) { cyclePath.unshift(current); current = parent.get(current); } cyclePath.unshift(dep); return cyclePath.join(' → '); } } inStack.delete(id); return null; } for (const artifact of artifacts) { if (!visited.has(artifact.id)) { const cycle = dfs(artifact.id); if (cycle) { throw new SchemaValidationError(`Cyclic dependency detected: ${cycle}`); } } } } //# sourceMappingURL=schema.js.map