Skip to content

Commit b278963

Browse files
committed
feat: support project-level schemas
1 parent bf4bc24 commit b278963

2 files changed

Lines changed: 311 additions & 9 deletions

File tree

src/core/artifact-graph/resolver.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export function getPackageSchemasDir(): string {
2929
return path.join(path.dirname(currentFile), '..', '..', '..', 'schemas');
3030
}
3131

32+
/**
33+
* Gets the project's local schemas directory path.
34+
*/
35+
export function getProjectSchemasDir(): string {
36+
return path.join(process.cwd(), 'openspec', 'schemas');
37+
}
38+
3239
/**
3340
* Gets the user's schema override directory path.
3441
*/
@@ -40,21 +47,29 @@ export function getUserSchemasDir(): string {
4047
* Resolves a schema name to its directory path.
4148
*
4249
* Resolution order:
43-
* 1. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
44-
* 2. Package built-in: <package>/schemas/<name>/schema.yaml
50+
* 1. Project-local: <cwd>/openspec/schemas/<name>/schema.yaml
51+
* 2. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
52+
* 3. Package built-in: <package>/schemas/<name>/schema.yaml
4553
*
4654
* @param name - Schema name (e.g., "spec-driven")
4755
* @returns The path to the schema directory, or null if not found
4856
*/
4957
export function getSchemaDir(name: string): string | null {
50-
// 1. Check user override directory
58+
// 1. Check project-local directory
59+
const projectDir = path.join(getProjectSchemasDir(), name);
60+
const projectSchemaPath = path.join(projectDir, 'schema.yaml');
61+
if (fs.existsSync(projectSchemaPath)) {
62+
return projectDir;
63+
}
64+
65+
// 2. Check user override directory
5166
const userDir = path.join(getUserSchemasDir(), name);
5267
const userSchemaPath = path.join(userDir, 'schema.yaml');
5368
if (fs.existsSync(userSchemaPath)) {
5469
return userDir;
5570
}
5671

57-
// 2. Check package built-in directory
72+
// 3. Check package built-in directory
5873
const packageDir = path.join(getPackageSchemasDir(), name);
5974
const packageSchemaPath = path.join(packageDir, 'schema.yaml');
6075
if (fs.existsSync(packageSchemaPath)) {
@@ -123,7 +138,7 @@ export function resolveSchema(name: string): SchemaYaml {
123138

124139
/**
125140
* Lists all available schema names.
126-
* Combines user override and package built-in schemas.
141+
* Combines project-local, user override and package built-in schemas.
127142
*/
128143
export function listSchemas(): string[] {
129144
const schemas = new Set<string>();
@@ -154,6 +169,19 @@ export function listSchemas(): string[] {
154169
}
155170
}
156171

172+
// Add project-local schemas (may override both user and package schemas)
173+
const projectDir = getProjectSchemasDir();
174+
if (fs.existsSync(projectDir)) {
175+
for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
176+
if (entry.isDirectory()) {
177+
const schemaPath = path.join(projectDir, entry.name, 'schema.yaml');
178+
if (fs.existsSync(schemaPath)) {
179+
schemas.add(entry.name);
180+
}
181+
}
182+
}
183+
}
184+
157185
return Array.from(schemas).sort();
158186
}
159187

@@ -164,7 +192,7 @@ export interface SchemaInfo {
164192
name: string;
165193
description: string;
166194
artifacts: string[];
167-
source: 'package' | 'user';
195+
source: 'package' | 'user' | 'project';
168196
}
169197

170198
/**
@@ -175,11 +203,35 @@ export function listSchemasWithInfo(): SchemaInfo[] {
175203
const schemas: SchemaInfo[] = [];
176204
const seenNames = new Set<string>();
177205

178-
// Add user override schemas first (they take precedence)
206+
// Add project-local schemas first (highest precedence)
207+
const projectDir = getProjectSchemasDir();
208+
if (fs.existsSync(projectDir)) {
209+
for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
210+
if (entry.isDirectory()) {
211+
const schemaPath = path.join(projectDir, entry.name, 'schema.yaml');
212+
if (fs.existsSync(schemaPath)) {
213+
try {
214+
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
215+
schemas.push({
216+
name: entry.name,
217+
description: schema.description || '',
218+
artifacts: schema.artifacts.map((a) => a.id),
219+
source: 'project',
220+
});
221+
seenNames.add(entry.name);
222+
} catch {
223+
// Skip invalid schemas
224+
}
225+
}
226+
}
227+
}
228+
}
229+
230+
// Add user override schemas next
179231
const userDir = getUserSchemasDir();
180232
if (fs.existsSync(userDir)) {
181233
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
182-
if (entry.isDirectory()) {
234+
if (entry.isDirectory() && !seenNames.has(entry.name)) {
183235
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
184236
if (fs.existsSync(schemaPath)) {
185237
try {

0 commit comments

Comments
 (0)