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