UNPKG

@fission-ai/openspec

Version:

AI-native system for spec-driven development

147 lines 5.47 kB
import * as fs from 'node:fs'; import * as path from 'node:path'; import * as yaml from 'yaml'; import { ChangeMetadataSchema } from '../core/artifact-graph/types.js'; import { listSchemas } from '../core/artifact-graph/resolver.js'; import { readProjectConfig } from '../core/project-config.js'; const METADATA_FILENAME = '.openspec.yaml'; /** * Error thrown when change metadata validation fails. */ export class ChangeMetadataError extends Error { metadataPath; cause; constructor(message, metadataPath, cause) { super(message); this.metadataPath = metadataPath; this.cause = cause; this.name = 'ChangeMetadataError'; } } /** * Validates that a schema name is valid (exists in available schemas). * * @param schemaName - The schema name to validate * @param projectRoot - Optional project root for project-local schema resolution * @returns The validated schema name * @throws Error if schema is not found */ export function validateSchemaName(schemaName, projectRoot) { const availableSchemas = listSchemas(projectRoot); if (!availableSchemas.includes(schemaName)) { throw new Error(`Unknown schema '${schemaName}'. Available: ${availableSchemas.join(', ')}`); } return schemaName; } /** * Writes change metadata to .openspec.yaml in the change directory. * * @param changeDir - The path to the change directory * @param metadata - The metadata to write * @param projectRoot - Optional project root for project-local schema resolution * @throws ChangeMetadataError if validation fails or write fails */ export function writeChangeMetadata(changeDir, metadata, projectRoot) { const metaPath = path.join(changeDir, METADATA_FILENAME); // Validate schema exists validateSchemaName(metadata.schema, projectRoot); // Validate with Zod const parseResult = ChangeMetadataSchema.safeParse(metadata); if (!parseResult.success) { throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath); } // Write YAML file const content = yaml.stringify(parseResult.data); try { fs.writeFileSync(metaPath, content, 'utf-8'); } catch (err) { const ioError = err instanceof Error ? err : new Error(String(err)); throw new ChangeMetadataError(`Failed to write metadata: ${ioError.message}`, metaPath, ioError); } } /** * Reads change metadata from .openspec.yaml in the change directory. * * @param changeDir - The path to the change directory * @param projectRoot - Optional project root for project-local schema resolution * @returns The validated metadata, or null if no metadata file exists * @throws ChangeMetadataError if the file exists but is invalid */ export function readChangeMetadata(changeDir, projectRoot) { const metaPath = path.join(changeDir, METADATA_FILENAME); if (!fs.existsSync(metaPath)) { return null; } let content; try { content = fs.readFileSync(metaPath, 'utf-8'); } catch (err) { const ioError = err instanceof Error ? err : new Error(String(err)); throw new ChangeMetadataError(`Failed to read metadata: ${ioError.message}`, metaPath, ioError); } let parsed; try { parsed = yaml.parse(content); } catch (err) { const parseError = err instanceof Error ? err : new Error(String(err)); throw new ChangeMetadataError(`Invalid YAML in metadata file: ${parseError.message}`, metaPath, parseError); } // Validate with Zod const parseResult = ChangeMetadataSchema.safeParse(parsed); if (!parseResult.success) { throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath); } // Validate that the schema exists const availableSchemas = listSchemas(projectRoot); if (!availableSchemas.includes(parseResult.data.schema)) { throw new ChangeMetadataError(`Unknown schema '${parseResult.data.schema}'. Available: ${availableSchemas.join(', ')}`, metaPath); } return parseResult.data; } /** * Resolves the schema for a change, with explicit override taking precedence. * * Resolution order: * 1. Explicit schema (if provided) * 2. Schema from .openspec.yaml metadata (if exists) * 3. Schema from openspec/config.yaml (if exists) * 4. Default 'spec-driven' * * @param changeDir - The path to the change directory * @param explicitSchema - Optional explicit schema override * @returns The resolved schema name */ export function resolveSchemaForChange(changeDir, explicitSchema) { // Derive project root from changeDir (changeDir is typically projectRoot/openspec/changes/change-name) const projectRoot = path.resolve(changeDir, '../../..'); // 1. Explicit override wins if (explicitSchema) { return explicitSchema; } // 2. Try reading from metadata try { const metadata = readChangeMetadata(changeDir, projectRoot); if (metadata?.schema) { return metadata.schema; } } catch { // If metadata read fails, continue to next option } // 3. Try reading from project config try { const config = readProjectConfig(projectRoot); if (config?.schema) { return config.schema; } } catch { // If config read fails, fall back to default } // 4. Default return 'spec-driven'; } //# sourceMappingURL=change-metadata.js.map