@fission-ai/openspec 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +7 -1
- package/dist/commands/artifact-workflow.d.ts +17 -0
- package/dist/commands/artifact-workflow.js +818 -0
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.js +3 -3
- package/dist/core/archive.d.ts +0 -5
- package/dist/core/archive.js +4 -257
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +7 -0
- package/dist/core/artifact-graph/index.js +13 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +130 -0
- package/dist/core/artifact-graph/instruction-loader.js +173 -0
- package/dist/core/artifact-graph/resolver.d.ts +61 -0
- package/dist/core/artifact-graph/resolver.js +187 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +54 -0
- package/dist/core/artifact-graph/types.d.ts +45 -0
- package/dist/core/artifact-graph/types.js +43 -0
- package/dist/core/converters/json-converter.js +2 -1
- package/dist/core/global-config.d.ts +10 -0
- package/dist/core/global-config.js +28 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/list.d.ts +6 -1
- package/dist/core/list.js +88 -6
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +384 -0
- package/dist/core/templates/skill-templates.d.ts +76 -0
- package/dist/core/templates/skill-templates.js +1472 -0
- package/dist/core/update.js +1 -1
- package/dist/core/validation/validator.js +2 -1
- package/dist/core/view.js +28 -8
- package/dist/utils/change-metadata.d.ts +47 -0
- package/dist/utils/change-metadata.js +130 -0
- package/dist/utils/change-utils.d.ts +51 -0
- package/dist/utils/change-utils.js +100 -0
- package/dist/utils/file-system.d.ts +5 -0
- package/dist/utils/file-system.js +7 -0
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +4 -1
- package/dist/utils/interactive.d.ts +7 -2
- package/dist/utils/interactive.js +9 -1
- package/package.json +4 -1
- package/schemas/spec-driven/schema.yaml +148 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +23 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +9 -0
- package/schemas/tdd/schema.yaml +213 -0
- package/schemas/tdd/templates/docs.md +15 -0
- package/schemas/tdd/templates/implementation.md +11 -0
- package/schemas/tdd/templates/spec.md +11 -0
- package/schemas/tdd/templates/test.md +11 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ArtifactGraph } from './graph.js';
|
|
2
|
+
import type { CompletedSet } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when loading a template fails.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TemplateLoadError extends Error {
|
|
7
|
+
readonly templatePath: string;
|
|
8
|
+
constructor(message: string, templatePath: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Change context containing graph, completion state, and metadata.
|
|
12
|
+
*/
|
|
13
|
+
export interface ChangeContext {
|
|
14
|
+
/** The artifact dependency graph */
|
|
15
|
+
graph: ArtifactGraph;
|
|
16
|
+
/** Set of completed artifact IDs */
|
|
17
|
+
completed: CompletedSet;
|
|
18
|
+
/** Schema name being used */
|
|
19
|
+
schemaName: string;
|
|
20
|
+
/** Change name */
|
|
21
|
+
changeName: string;
|
|
22
|
+
/** Path to the change directory */
|
|
23
|
+
changeDir: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Enriched instructions for creating an artifact.
|
|
27
|
+
*/
|
|
28
|
+
export interface ArtifactInstructions {
|
|
29
|
+
/** Change name */
|
|
30
|
+
changeName: string;
|
|
31
|
+
/** Artifact ID */
|
|
32
|
+
artifactId: string;
|
|
33
|
+
/** Schema name */
|
|
34
|
+
schemaName: string;
|
|
35
|
+
/** Full path to change directory */
|
|
36
|
+
changeDir: string;
|
|
37
|
+
/** Output path pattern (e.g., "proposal.md") */
|
|
38
|
+
outputPath: string;
|
|
39
|
+
/** Artifact description */
|
|
40
|
+
description: string;
|
|
41
|
+
/** Guidance on how to create this artifact (from schema instruction field) */
|
|
42
|
+
instruction: string | undefined;
|
|
43
|
+
/** Template content (structure to follow) */
|
|
44
|
+
template: string;
|
|
45
|
+
/** Dependencies with completion status and paths */
|
|
46
|
+
dependencies: DependencyInfo[];
|
|
47
|
+
/** Artifacts that become available after completing this one */
|
|
48
|
+
unlocks: string[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Dependency information including path and description.
|
|
52
|
+
*/
|
|
53
|
+
export interface DependencyInfo {
|
|
54
|
+
/** Artifact ID */
|
|
55
|
+
id: string;
|
|
56
|
+
/** Whether the dependency is completed */
|
|
57
|
+
done: boolean;
|
|
58
|
+
/** Relative output path of the dependency (e.g., "proposal.md") */
|
|
59
|
+
path: string;
|
|
60
|
+
/** Description of the dependency artifact */
|
|
61
|
+
description: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Status of a single artifact in the workflow.
|
|
65
|
+
*/
|
|
66
|
+
export interface ArtifactStatus {
|
|
67
|
+
/** Artifact ID */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Output path pattern */
|
|
70
|
+
outputPath: string;
|
|
71
|
+
/** Status: done, ready, or blocked */
|
|
72
|
+
status: 'done' | 'ready' | 'blocked';
|
|
73
|
+
/** Missing dependencies (only for blocked) */
|
|
74
|
+
missingDeps?: string[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Formatted change status.
|
|
78
|
+
*/
|
|
79
|
+
export interface ChangeStatus {
|
|
80
|
+
/** Change name */
|
|
81
|
+
changeName: string;
|
|
82
|
+
/** Schema name */
|
|
83
|
+
schemaName: string;
|
|
84
|
+
/** Whether all artifacts are complete */
|
|
85
|
+
isComplete: boolean;
|
|
86
|
+
/** Artifact IDs required before apply phase (from schema's apply.requires) */
|
|
87
|
+
applyRequires: string[];
|
|
88
|
+
/** Status of each artifact */
|
|
89
|
+
artifacts: ArtifactStatus[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Loads a template from a schema's templates directory.
|
|
93
|
+
*
|
|
94
|
+
* @param schemaName - Schema name (e.g., "spec-driven")
|
|
95
|
+
* @param templatePath - Relative path within the templates directory (e.g., "proposal.md")
|
|
96
|
+
* @returns The template content
|
|
97
|
+
* @throws TemplateLoadError if the template cannot be loaded
|
|
98
|
+
*/
|
|
99
|
+
export declare function loadTemplate(schemaName: string, templatePath: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Loads change context combining graph and completion state.
|
|
102
|
+
*
|
|
103
|
+
* Schema resolution order:
|
|
104
|
+
* 1. Explicit schemaName parameter (if provided)
|
|
105
|
+
* 2. Schema from .openspec.yaml metadata (if exists in change directory)
|
|
106
|
+
* 3. Default 'spec-driven'
|
|
107
|
+
*
|
|
108
|
+
* @param projectRoot - Project root directory
|
|
109
|
+
* @param changeName - Change name
|
|
110
|
+
* @param schemaName - Optional schema name override. If not provided, auto-detected from metadata.
|
|
111
|
+
* @returns Change context with graph, completed set, and metadata
|
|
112
|
+
*/
|
|
113
|
+
export declare function loadChangeContext(projectRoot: string, changeName: string, schemaName?: string): ChangeContext;
|
|
114
|
+
/**
|
|
115
|
+
* Generates enriched instructions for creating an artifact.
|
|
116
|
+
*
|
|
117
|
+
* @param context - Change context
|
|
118
|
+
* @param artifactId - Artifact ID to generate instructions for
|
|
119
|
+
* @returns Enriched artifact instructions
|
|
120
|
+
* @throws Error if artifact not found
|
|
121
|
+
*/
|
|
122
|
+
export declare function generateInstructions(context: ChangeContext, artifactId: string): ArtifactInstructions;
|
|
123
|
+
/**
|
|
124
|
+
* Formats the status of all artifacts in a change.
|
|
125
|
+
*
|
|
126
|
+
* @param context - Change context
|
|
127
|
+
* @returns Formatted change status
|
|
128
|
+
*/
|
|
129
|
+
export declare function formatChangeStatus(context: ChangeContext): ChangeStatus;
|
|
130
|
+
//# sourceMappingURL=instruction-loader.d.ts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getSchemaDir, resolveSchema } from './resolver.js';
|
|
4
|
+
import { ArtifactGraph } from './graph.js';
|
|
5
|
+
import { detectCompleted } from './state.js';
|
|
6
|
+
import { resolveSchemaForChange } from '../../utils/change-metadata.js';
|
|
7
|
+
/**
|
|
8
|
+
* Error thrown when loading a template fails.
|
|
9
|
+
*/
|
|
10
|
+
export class TemplateLoadError extends Error {
|
|
11
|
+
templatePath;
|
|
12
|
+
constructor(message, templatePath) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.templatePath = templatePath;
|
|
15
|
+
this.name = 'TemplateLoadError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Loads a template from a schema's templates directory.
|
|
20
|
+
*
|
|
21
|
+
* @param schemaName - Schema name (e.g., "spec-driven")
|
|
22
|
+
* @param templatePath - Relative path within the templates directory (e.g., "proposal.md")
|
|
23
|
+
* @returns The template content
|
|
24
|
+
* @throws TemplateLoadError if the template cannot be loaded
|
|
25
|
+
*/
|
|
26
|
+
export function loadTemplate(schemaName, templatePath) {
|
|
27
|
+
const schemaDir = getSchemaDir(schemaName);
|
|
28
|
+
if (!schemaDir) {
|
|
29
|
+
throw new TemplateLoadError(`Schema '${schemaName}' not found`, templatePath);
|
|
30
|
+
}
|
|
31
|
+
const fullPath = path.join(schemaDir, 'templates', templatePath);
|
|
32
|
+
if (!fs.existsSync(fullPath)) {
|
|
33
|
+
throw new TemplateLoadError(`Template not found: ${fullPath}`, fullPath);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return fs.readFileSync(fullPath, 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const ioError = err instanceof Error ? err : new Error(String(err));
|
|
40
|
+
throw new TemplateLoadError(`Failed to read template: ${ioError.message}`, fullPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Loads change context combining graph and completion state.
|
|
45
|
+
*
|
|
46
|
+
* Schema resolution order:
|
|
47
|
+
* 1. Explicit schemaName parameter (if provided)
|
|
48
|
+
* 2. Schema from .openspec.yaml metadata (if exists in change directory)
|
|
49
|
+
* 3. Default 'spec-driven'
|
|
50
|
+
*
|
|
51
|
+
* @param projectRoot - Project root directory
|
|
52
|
+
* @param changeName - Change name
|
|
53
|
+
* @param schemaName - Optional schema name override. If not provided, auto-detected from metadata.
|
|
54
|
+
* @returns Change context with graph, completed set, and metadata
|
|
55
|
+
*/
|
|
56
|
+
export function loadChangeContext(projectRoot, changeName, schemaName) {
|
|
57
|
+
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
|
|
58
|
+
// Resolve schema: explicit > metadata > default
|
|
59
|
+
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName);
|
|
60
|
+
const schema = resolveSchema(resolvedSchemaName);
|
|
61
|
+
const graph = ArtifactGraph.fromSchema(schema);
|
|
62
|
+
const completed = detectCompleted(graph, changeDir);
|
|
63
|
+
return {
|
|
64
|
+
graph,
|
|
65
|
+
completed,
|
|
66
|
+
schemaName: resolvedSchemaName,
|
|
67
|
+
changeName,
|
|
68
|
+
changeDir,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Generates enriched instructions for creating an artifact.
|
|
73
|
+
*
|
|
74
|
+
* @param context - Change context
|
|
75
|
+
* @param artifactId - Artifact ID to generate instructions for
|
|
76
|
+
* @returns Enriched artifact instructions
|
|
77
|
+
* @throws Error if artifact not found
|
|
78
|
+
*/
|
|
79
|
+
export function generateInstructions(context, artifactId) {
|
|
80
|
+
const artifact = context.graph.getArtifact(artifactId);
|
|
81
|
+
if (!artifact) {
|
|
82
|
+
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
|
|
83
|
+
}
|
|
84
|
+
const template = loadTemplate(context.schemaName, artifact.template);
|
|
85
|
+
const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
|
|
86
|
+
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
|
|
87
|
+
return {
|
|
88
|
+
changeName: context.changeName,
|
|
89
|
+
artifactId: artifact.id,
|
|
90
|
+
schemaName: context.schemaName,
|
|
91
|
+
changeDir: context.changeDir,
|
|
92
|
+
outputPath: artifact.generates,
|
|
93
|
+
description: artifact.description,
|
|
94
|
+
instruction: artifact.instruction,
|
|
95
|
+
template,
|
|
96
|
+
dependencies,
|
|
97
|
+
unlocks,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Gets dependency info including paths and descriptions.
|
|
102
|
+
*/
|
|
103
|
+
function getDependencyInfo(artifact, graph, completed) {
|
|
104
|
+
return artifact.requires.map(id => {
|
|
105
|
+
const depArtifact = graph.getArtifact(id);
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
done: completed.has(id),
|
|
109
|
+
path: depArtifact?.generates ?? id,
|
|
110
|
+
description: depArtifact?.description ?? '',
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Gets artifacts that become available after completing the given artifact.
|
|
116
|
+
*/
|
|
117
|
+
function getUnlockedArtifacts(graph, artifactId) {
|
|
118
|
+
const unlocks = [];
|
|
119
|
+
for (const artifact of graph.getAllArtifacts()) {
|
|
120
|
+
if (artifact.requires.includes(artifactId)) {
|
|
121
|
+
unlocks.push(artifact.id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return unlocks.sort();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Formats the status of all artifacts in a change.
|
|
128
|
+
*
|
|
129
|
+
* @param context - Change context
|
|
130
|
+
* @returns Formatted change status
|
|
131
|
+
*/
|
|
132
|
+
export function formatChangeStatus(context) {
|
|
133
|
+
// Load schema to get apply phase configuration
|
|
134
|
+
const schema = resolveSchema(context.schemaName);
|
|
135
|
+
const applyRequires = schema.apply?.requires ?? schema.artifacts.map(a => a.id);
|
|
136
|
+
const artifacts = context.graph.getAllArtifacts();
|
|
137
|
+
const ready = new Set(context.graph.getNextArtifacts(context.completed));
|
|
138
|
+
const blocked = context.graph.getBlocked(context.completed);
|
|
139
|
+
const artifactStatuses = artifacts.map(artifact => {
|
|
140
|
+
if (context.completed.has(artifact.id)) {
|
|
141
|
+
return {
|
|
142
|
+
id: artifact.id,
|
|
143
|
+
outputPath: artifact.generates,
|
|
144
|
+
status: 'done',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (ready.has(artifact.id)) {
|
|
148
|
+
return {
|
|
149
|
+
id: artifact.id,
|
|
150
|
+
outputPath: artifact.generates,
|
|
151
|
+
status: 'ready',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
id: artifact.id,
|
|
156
|
+
outputPath: artifact.generates,
|
|
157
|
+
status: 'blocked',
|
|
158
|
+
missingDeps: blocked[artifact.id] ?? [],
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
// Sort by build order for consistent output
|
|
162
|
+
const buildOrder = context.graph.getBuildOrder();
|
|
163
|
+
const orderMap = new Map(buildOrder.map((id, idx) => [id, idx]));
|
|
164
|
+
artifactStatuses.sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0));
|
|
165
|
+
return {
|
|
166
|
+
changeName: context.changeName,
|
|
167
|
+
schemaName: context.schemaName,
|
|
168
|
+
isComplete: context.graph.isComplete(context.completed),
|
|
169
|
+
applyRequires,
|
|
170
|
+
artifacts: artifactStatuses,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=instruction-loader.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SchemaYaml } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when loading a schema fails.
|
|
4
|
+
*/
|
|
5
|
+
export declare class SchemaLoadError extends Error {
|
|
6
|
+
readonly schemaPath: string;
|
|
7
|
+
readonly cause?: Error | undefined;
|
|
8
|
+
constructor(message: string, schemaPath: string, cause?: Error | undefined);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Gets the package's built-in schemas directory path.
|
|
12
|
+
* Uses import.meta.url to resolve relative to the current module.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getPackageSchemasDir(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Gets the user's schema override directory path.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getUserSchemasDir(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a schema name to its directory path.
|
|
21
|
+
*
|
|
22
|
+
* Resolution order:
|
|
23
|
+
* 1. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
|
|
24
|
+
* 2. Package built-in: <package>/schemas/<name>/schema.yaml
|
|
25
|
+
*
|
|
26
|
+
* @param name - Schema name (e.g., "spec-driven")
|
|
27
|
+
* @returns The path to the schema directory, or null if not found
|
|
28
|
+
*/
|
|
29
|
+
export declare function getSchemaDir(name: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Resolves a schema name to a SchemaYaml object.
|
|
32
|
+
*
|
|
33
|
+
* Resolution order:
|
|
34
|
+
* 1. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
|
|
35
|
+
* 2. Package built-in: <package>/schemas/<name>/schema.yaml
|
|
36
|
+
*
|
|
37
|
+
* @param name - Schema name (e.g., "spec-driven")
|
|
38
|
+
* @returns The resolved schema object
|
|
39
|
+
* @throws Error if schema is not found in any location
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveSchema(name: string): SchemaYaml;
|
|
42
|
+
/**
|
|
43
|
+
* Lists all available schema names.
|
|
44
|
+
* Combines user override and package built-in schemas.
|
|
45
|
+
*/
|
|
46
|
+
export declare function listSchemas(): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Schema info with metadata (name, description, artifacts).
|
|
49
|
+
*/
|
|
50
|
+
export interface SchemaInfo {
|
|
51
|
+
name: string;
|
|
52
|
+
description: string;
|
|
53
|
+
artifacts: string[];
|
|
54
|
+
source: 'package' | 'user';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Lists all available schemas with their descriptions and artifact lists.
|
|
58
|
+
* Useful for agent skills to present schema selection to users.
|
|
59
|
+
*/
|
|
60
|
+
export declare function listSchemasWithInfo(): SchemaInfo[];
|
|
61
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { getGlobalDataDir } from '../global-config.js';
|
|
5
|
+
import { parseSchema, SchemaValidationError } from './schema.js';
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown when loading a schema fails.
|
|
8
|
+
*/
|
|
9
|
+
export class SchemaLoadError extends Error {
|
|
10
|
+
schemaPath;
|
|
11
|
+
cause;
|
|
12
|
+
constructor(message, schemaPath, cause) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.schemaPath = schemaPath;
|
|
15
|
+
this.cause = cause;
|
|
16
|
+
this.name = 'SchemaLoadError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Gets the package's built-in schemas directory path.
|
|
21
|
+
* Uses import.meta.url to resolve relative to the current module.
|
|
22
|
+
*/
|
|
23
|
+
export function getPackageSchemasDir() {
|
|
24
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
25
|
+
// Navigate from dist/core/artifact-graph/ to package root's schemas/
|
|
26
|
+
return path.join(path.dirname(currentFile), '..', '..', '..', 'schemas');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Gets the user's schema override directory path.
|
|
30
|
+
*/
|
|
31
|
+
export function getUserSchemasDir() {
|
|
32
|
+
return path.join(getGlobalDataDir(), 'schemas');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolves a schema name to its directory path.
|
|
36
|
+
*
|
|
37
|
+
* Resolution order:
|
|
38
|
+
* 1. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
|
|
39
|
+
* 2. Package built-in: <package>/schemas/<name>/schema.yaml
|
|
40
|
+
*
|
|
41
|
+
* @param name - Schema name (e.g., "spec-driven")
|
|
42
|
+
* @returns The path to the schema directory, or null if not found
|
|
43
|
+
*/
|
|
44
|
+
export function getSchemaDir(name) {
|
|
45
|
+
// 1. Check user override directory
|
|
46
|
+
const userDir = path.join(getUserSchemasDir(), name);
|
|
47
|
+
const userSchemaPath = path.join(userDir, 'schema.yaml');
|
|
48
|
+
if (fs.existsSync(userSchemaPath)) {
|
|
49
|
+
return userDir;
|
|
50
|
+
}
|
|
51
|
+
// 2. Check package built-in directory
|
|
52
|
+
const packageDir = path.join(getPackageSchemasDir(), name);
|
|
53
|
+
const packageSchemaPath = path.join(packageDir, 'schema.yaml');
|
|
54
|
+
if (fs.existsSync(packageSchemaPath)) {
|
|
55
|
+
return packageDir;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves a schema name to a SchemaYaml object.
|
|
61
|
+
*
|
|
62
|
+
* Resolution order:
|
|
63
|
+
* 1. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
|
|
64
|
+
* 2. Package built-in: <package>/schemas/<name>/schema.yaml
|
|
65
|
+
*
|
|
66
|
+
* @param name - Schema name (e.g., "spec-driven")
|
|
67
|
+
* @returns The resolved schema object
|
|
68
|
+
* @throws Error if schema is not found in any location
|
|
69
|
+
*/
|
|
70
|
+
export function resolveSchema(name) {
|
|
71
|
+
// Normalize name (remove .yaml extension if provided)
|
|
72
|
+
const normalizedName = name.replace(/\.ya?ml$/, '');
|
|
73
|
+
const schemaDir = getSchemaDir(normalizedName);
|
|
74
|
+
if (!schemaDir) {
|
|
75
|
+
const availableSchemas = listSchemas();
|
|
76
|
+
throw new Error(`Schema '${normalizedName}' not found. Available schemas: ${availableSchemas.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
const schemaPath = path.join(schemaDir, 'schema.yaml');
|
|
79
|
+
// Load and parse the schema
|
|
80
|
+
let content;
|
|
81
|
+
try {
|
|
82
|
+
content = fs.readFileSync(schemaPath, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const ioError = err instanceof Error ? err : new Error(String(err));
|
|
86
|
+
throw new SchemaLoadError(`Failed to read schema at '${schemaPath}': ${ioError.message}`, schemaPath, ioError);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return parseSchema(content);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err instanceof SchemaValidationError) {
|
|
93
|
+
throw new SchemaLoadError(`Invalid schema at '${schemaPath}': ${err.message}`, schemaPath, err);
|
|
94
|
+
}
|
|
95
|
+
const parseError = err instanceof Error ? err : new Error(String(err));
|
|
96
|
+
throw new SchemaLoadError(`Failed to parse schema at '${schemaPath}': ${parseError.message}`, schemaPath, parseError);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Lists all available schema names.
|
|
101
|
+
* Combines user override and package built-in schemas.
|
|
102
|
+
*/
|
|
103
|
+
export function listSchemas() {
|
|
104
|
+
const schemas = new Set();
|
|
105
|
+
// Add package built-in schemas
|
|
106
|
+
const packageDir = getPackageSchemasDir();
|
|
107
|
+
if (fs.existsSync(packageDir)) {
|
|
108
|
+
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
|
|
111
|
+
if (fs.existsSync(schemaPath)) {
|
|
112
|
+
schemas.add(entry.name);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Add user override schemas (may override package schemas)
|
|
118
|
+
const userDir = getUserSchemasDir();
|
|
119
|
+
if (fs.existsSync(userDir)) {
|
|
120
|
+
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
|
|
123
|
+
if (fs.existsSync(schemaPath)) {
|
|
124
|
+
schemas.add(entry.name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return Array.from(schemas).sort();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Lists all available schemas with their descriptions and artifact lists.
|
|
133
|
+
* Useful for agent skills to present schema selection to users.
|
|
134
|
+
*/
|
|
135
|
+
export function listSchemasWithInfo() {
|
|
136
|
+
const schemas = [];
|
|
137
|
+
const seenNames = new Set();
|
|
138
|
+
// Add user override schemas first (they take precedence)
|
|
139
|
+
const userDir = getUserSchemasDir();
|
|
140
|
+
if (fs.existsSync(userDir)) {
|
|
141
|
+
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
|
|
144
|
+
if (fs.existsSync(schemaPath)) {
|
|
145
|
+
try {
|
|
146
|
+
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
|
|
147
|
+
schemas.push({
|
|
148
|
+
name: entry.name,
|
|
149
|
+
description: schema.description || '',
|
|
150
|
+
artifacts: schema.artifacts.map((a) => a.id),
|
|
151
|
+
source: 'user',
|
|
152
|
+
});
|
|
153
|
+
seenNames.add(entry.name);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Skip invalid schemas
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Add package built-in schemas (if not overridden)
|
|
163
|
+
const packageDir = getPackageSchemasDir();
|
|
164
|
+
if (fs.existsSync(packageDir)) {
|
|
165
|
+
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
|
|
166
|
+
if (entry.isDirectory() && !seenNames.has(entry.name)) {
|
|
167
|
+
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
|
|
168
|
+
if (fs.existsSync(schemaPath)) {
|
|
169
|
+
try {
|
|
170
|
+
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
|
|
171
|
+
schemas.push({
|
|
172
|
+
name: entry.name,
|
|
173
|
+
description: schema.description || '',
|
|
174
|
+
artifacts: schema.artifacts.map((a) => a.id),
|
|
175
|
+
source: 'package',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Skip invalid schemas
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return schemas.sort((a, b) => a.name.localeCompare(b.name));
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SchemaYaml } from './types.js';
|
|
2
|
+
export declare class SchemaValidationError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Loads and validates an artifact schema from a YAML file.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadSchema(filePath: string): SchemaYaml;
|
|
9
|
+
/**
|
|
10
|
+
* Parses and validates an artifact schema from YAML content.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseSchema(yamlContent: string): SchemaYaml;
|
|
13
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
|
+
import { SchemaYamlSchema } from './types.js';
|
|
4
|
+
export class SchemaValidationError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'SchemaValidationError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Loads and validates an artifact schema from a YAML file.
|
|
12
|
+
*/
|
|
13
|
+
export function loadSchema(filePath) {
|
|
14
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
15
|
+
return parseSchema(content);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parses and validates an artifact schema from YAML content.
|
|
19
|
+
*/
|
|
20
|
+
export function parseSchema(yamlContent) {
|
|
21
|
+
const parsed = parseYaml(yamlContent);
|
|
22
|
+
// Validate with Zod
|
|
23
|
+
const result = SchemaYamlSchema.safeParse(parsed);
|
|
24
|
+
if (!result.success) {
|
|
25
|
+
const errors = result.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
|
26
|
+
throw new SchemaValidationError(`Invalid schema: ${errors}`);
|
|
27
|
+
}
|
|
28
|
+
const schema = result.data;
|
|
29
|
+
// Check for duplicate artifact IDs
|
|
30
|
+
validateNoDuplicateIds(schema.artifacts);
|
|
31
|
+
// Check that all requires references are valid
|
|
32
|
+
validateRequiresReferences(schema.artifacts);
|
|
33
|
+
// Check for cycles
|
|
34
|
+
validateNoCycles(schema.artifacts);
|
|
35
|
+
return schema;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validates that there are no duplicate artifact IDs.
|
|
39
|
+
*/
|
|
40
|
+
function validateNoDuplicateIds(artifacts) {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (const artifact of artifacts) {
|
|
43
|
+
if (seen.has(artifact.id)) {
|
|
44
|
+
throw new SchemaValidationError(`Duplicate artifact ID: ${artifact.id}`);
|
|
45
|
+
}
|
|
46
|
+
seen.add(artifact.id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validates that all `requires` references point to valid artifact IDs.
|
|
51
|
+
*/
|
|
52
|
+
function validateRequiresReferences(artifacts) {
|
|
53
|
+
const validIds = new Set(artifacts.map(a => a.id));
|
|
54
|
+
for (const artifact of artifacts) {
|
|
55
|
+
for (const req of artifact.requires) {
|
|
56
|
+
if (!validIds.has(req)) {
|
|
57
|
+
throw new SchemaValidationError(`Invalid dependency reference in artifact '${artifact.id}': '${req}' does not exist`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validates that there are no cyclic dependencies.
|
|
64
|
+
* Uses DFS to detect cycles and reports the full cycle path.
|
|
65
|
+
*/
|
|
66
|
+
function validateNoCycles(artifacts) {
|
|
67
|
+
const artifactMap = new Map(artifacts.map(a => [a.id, a]));
|
|
68
|
+
const visited = new Set();
|
|
69
|
+
const inStack = new Set();
|
|
70
|
+
const parent = new Map();
|
|
71
|
+
function dfs(id) {
|
|
72
|
+
visited.add(id);
|
|
73
|
+
inStack.add(id);
|
|
74
|
+
const artifact = artifactMap.get(id);
|
|
75
|
+
if (!artifact)
|
|
76
|
+
return null;
|
|
77
|
+
for (const dep of artifact.requires) {
|
|
78
|
+
if (!visited.has(dep)) {
|
|
79
|
+
parent.set(dep, id);
|
|
80
|
+
const cycle = dfs(dep);
|
|
81
|
+
if (cycle)
|
|
82
|
+
return cycle;
|
|
83
|
+
}
|
|
84
|
+
else if (inStack.has(dep)) {
|
|
85
|
+
// Found a cycle - reconstruct the path
|
|
86
|
+
const cyclePath = [dep];
|
|
87
|
+
let current = id;
|
|
88
|
+
while (current !== dep) {
|
|
89
|
+
cyclePath.unshift(current);
|
|
90
|
+
current = parent.get(current);
|
|
91
|
+
}
|
|
92
|
+
cyclePath.unshift(dep);
|
|
93
|
+
return cyclePath.join(' → ');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
inStack.delete(id);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
for (const artifact of artifacts) {
|
|
100
|
+
if (!visited.has(artifact.id)) {
|
|
101
|
+
const cycle = dfs(artifact.id);
|
|
102
|
+
if (cycle) {
|
|
103
|
+
throw new SchemaValidationError(`Cyclic dependency detected: ${cycle}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CompletedSet } from './types.js';
|
|
2
|
+
import type { ArtifactGraph } from './graph.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects which artifacts are completed by checking file existence in the change directory.
|
|
5
|
+
* Returns a Set of completed artifact IDs.
|
|
6
|
+
*
|
|
7
|
+
* @param graph - The artifact graph to check
|
|
8
|
+
* @param changeDir - The change directory to scan for files
|
|
9
|
+
* @returns Set of artifact IDs whose generated files exist
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectCompleted(graph: ArtifactGraph, changeDir: string): CompletedSet;
|
|
12
|
+
//# sourceMappingURL=state.d.ts.map
|