@fission-ai/openspec
Version:
AI-native system for spec-driven development
108 lines • 3.57 kB
JavaScript
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