@fission-ai/openspec 0.21.0 → 0.22.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 +2 -0
- package/dist/commands/artifact-workflow.js +155 -22
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.js +869 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +11 -2
- package/dist/core/artifact-graph/instruction-loader.js +59 -7
- package/dist/core/artifact-graph/resolver.d.ts +32 -12
- package/dist/core/artifact-graph/resolver.js +88 -18
- package/dist/core/completions/command-registry.js +76 -0
- package/dist/core/completions/types.d.ts +2 -1
- package/dist/core/config-prompts.d.ts +36 -0
- package/dist/core/config-prompts.js +151 -0
- package/dist/core/project-config.d.ts +64 -0
- package/dist/core/project-config.js +223 -0
- package/dist/utils/change-metadata.d.ts +8 -4
- package/dist/utils/change-metadata.js +27 -10
- package/dist/utils/change-utils.d.ts +14 -3
- package/dist/utils/change-utils.js +27 -6
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
2
|
+
import { listSchemasWithInfo, resolveSchema } from './artifact-graph/resolver.js';
|
|
3
|
+
/**
|
|
4
|
+
* Check if an error is an ExitPromptError (user cancelled with Ctrl+C).
|
|
5
|
+
* Used instead of instanceof check since @inquirer modules use dynamic imports.
|
|
6
|
+
*/
|
|
7
|
+
export function isExitPromptError(error) {
|
|
8
|
+
return (error !== null &&
|
|
9
|
+
typeof error === 'object' &&
|
|
10
|
+
'name' in error &&
|
|
11
|
+
error.name === 'ExitPromptError');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Prompt user to create project config interactively.
|
|
15
|
+
* Used by experimental setup command.
|
|
16
|
+
*
|
|
17
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
18
|
+
* @returns Config prompt result
|
|
19
|
+
* @throws ExitPromptError if user cancels (Ctrl+C)
|
|
20
|
+
*/
|
|
21
|
+
export async function promptForConfig(projectRoot) {
|
|
22
|
+
// Dynamic imports to prevent pre-commit hook hangs (see #367)
|
|
23
|
+
const { confirm, select, editor, checkbox } = await import('@inquirer/prompts');
|
|
24
|
+
// Ask if user wants to create config
|
|
25
|
+
const shouldCreate = await confirm({
|
|
26
|
+
message: 'Create openspec/config.yaml?',
|
|
27
|
+
default: true,
|
|
28
|
+
});
|
|
29
|
+
if (!shouldCreate) {
|
|
30
|
+
return { createConfig: false };
|
|
31
|
+
}
|
|
32
|
+
// Get available schemas
|
|
33
|
+
const schemas = listSchemasWithInfo(projectRoot);
|
|
34
|
+
if (schemas.length === 0) {
|
|
35
|
+
throw new Error('No schemas found. Cannot create config.');
|
|
36
|
+
}
|
|
37
|
+
// Prompt for schema selection
|
|
38
|
+
const selectedSchema = await select({
|
|
39
|
+
message: 'Default schema for new changes?',
|
|
40
|
+
choices: schemas.map((s) => ({
|
|
41
|
+
name: `${s.name} (${s.artifacts.join(' → ')})`,
|
|
42
|
+
value: s.name,
|
|
43
|
+
description: s.description || undefined,
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
// Prompt for project context
|
|
47
|
+
console.log('\nAdd project context? (optional)');
|
|
48
|
+
console.log('Context is shown to AI when creating artifacts.');
|
|
49
|
+
console.log('Examples: tech stack, conventions, style guides, domain knowledge\n');
|
|
50
|
+
const contextInput = await editor({
|
|
51
|
+
message: 'Press Enter to skip, or edit context:',
|
|
52
|
+
default: '',
|
|
53
|
+
waitForUseInput: false,
|
|
54
|
+
});
|
|
55
|
+
const context = contextInput.trim() || undefined;
|
|
56
|
+
// Prompt for per-artifact rules
|
|
57
|
+
const addRules = await confirm({
|
|
58
|
+
message: 'Add per-artifact rules? (optional)',
|
|
59
|
+
default: false,
|
|
60
|
+
});
|
|
61
|
+
let rules;
|
|
62
|
+
if (addRules) {
|
|
63
|
+
// Load the selected schema to get artifact list
|
|
64
|
+
const schema = resolveSchema(selectedSchema, projectRoot);
|
|
65
|
+
const artifactIds = schema.artifacts.map((a) => a.id);
|
|
66
|
+
// Let user select which artifacts to add rules for
|
|
67
|
+
const selectedArtifacts = await checkbox({
|
|
68
|
+
message: 'Which artifacts should have custom rules?',
|
|
69
|
+
choices: artifactIds.map((id) => ({
|
|
70
|
+
name: id,
|
|
71
|
+
value: id,
|
|
72
|
+
})),
|
|
73
|
+
});
|
|
74
|
+
if (selectedArtifacts.length > 0) {
|
|
75
|
+
rules = {};
|
|
76
|
+
// For each selected artifact, collect rules line by line
|
|
77
|
+
for (const artifactId of selectedArtifacts) {
|
|
78
|
+
const artifactRules = await promptForArtifactRules(artifactId);
|
|
79
|
+
if (artifactRules.length > 0) {
|
|
80
|
+
rules[artifactId] = artifactRules;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// If no rules were actually added, set to undefined
|
|
84
|
+
if (Object.keys(rules).length === 0) {
|
|
85
|
+
rules = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
createConfig: true,
|
|
91
|
+
schema: selectedSchema,
|
|
92
|
+
context,
|
|
93
|
+
rules,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Prompt for rules for a specific artifact.
|
|
98
|
+
* Collects rules one per line until user enters empty line.
|
|
99
|
+
*
|
|
100
|
+
* @param artifactId - The artifact ID to collect rules for
|
|
101
|
+
* @returns Array of rules
|
|
102
|
+
*/
|
|
103
|
+
async function promptForArtifactRules(artifactId) {
|
|
104
|
+
// Dynamic import to prevent pre-commit hook hangs (see #367)
|
|
105
|
+
const { input } = await import('@inquirer/prompts');
|
|
106
|
+
const rules = [];
|
|
107
|
+
console.log(`\nRules for ${artifactId} artifact:`);
|
|
108
|
+
console.log('Enter rules one per line, press Enter on empty line to finish:\n');
|
|
109
|
+
while (true) {
|
|
110
|
+
const rule = await input({
|
|
111
|
+
message: '│',
|
|
112
|
+
validate: () => {
|
|
113
|
+
// Empty string is valid (signals end of input)
|
|
114
|
+
return true;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const trimmed = rule.trim();
|
|
118
|
+
// Empty line signals end of input
|
|
119
|
+
if (!trimmed) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
rules.push(trimmed);
|
|
123
|
+
}
|
|
124
|
+
return rules;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Serialize config to YAML string with proper multi-line formatting.
|
|
128
|
+
*
|
|
129
|
+
* @param config - Partial config object (schema required, context/rules optional)
|
|
130
|
+
* @returns YAML string ready to write to file
|
|
131
|
+
*/
|
|
132
|
+
export function serializeConfig(config) {
|
|
133
|
+
// Build clean config object (only include defined fields)
|
|
134
|
+
const cleanConfig = {
|
|
135
|
+
schema: config.schema,
|
|
136
|
+
};
|
|
137
|
+
if (config.context) {
|
|
138
|
+
cleanConfig.context = config.context;
|
|
139
|
+
}
|
|
140
|
+
if (config.rules && Object.keys(config.rules).length > 0) {
|
|
141
|
+
cleanConfig.rules = config.rules;
|
|
142
|
+
}
|
|
143
|
+
// Serialize to YAML with proper formatting
|
|
144
|
+
return stringifyYaml(cleanConfig, {
|
|
145
|
+
indent: 2,
|
|
146
|
+
lineWidth: 0, // Don't wrap long lines
|
|
147
|
+
defaultStringType: 'PLAIN',
|
|
148
|
+
defaultKeyType: 'PLAIN',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=config-prompts.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for project configuration.
|
|
4
|
+
*
|
|
5
|
+
* Purpose:
|
|
6
|
+
* 1. Documentation - clearly defines the config file structure
|
|
7
|
+
* 2. Type safety - TypeScript infers ProjectConfig type from schema
|
|
8
|
+
* 3. Runtime validation - uses safeParse() for resilient field-by-field validation
|
|
9
|
+
*
|
|
10
|
+
* Why Zod over manual validation:
|
|
11
|
+
* - Helps understand OpenSpec's data interfaces at a glance
|
|
12
|
+
* - Single source of truth for type and validation
|
|
13
|
+
* - Consistent with other OpenSpec schemas
|
|
14
|
+
*/
|
|
15
|
+
export declare const ProjectConfigSchema: z.ZodObject<{
|
|
16
|
+
schema: z.ZodString;
|
|
17
|
+
context: z.ZodOptional<z.ZodString>;
|
|
18
|
+
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
|
|
21
|
+
/**
|
|
22
|
+
* Read and parse openspec/config.yaml from project root.
|
|
23
|
+
* Uses resilient parsing - validates each field independently using Zod safeParse.
|
|
24
|
+
* Returns null if file doesn't exist.
|
|
25
|
+
* Returns partial config if some fields are invalid (with warnings).
|
|
26
|
+
*
|
|
27
|
+
* Performance note (Jan 2025):
|
|
28
|
+
* Benchmarks showed direct file reads are fast enough without caching:
|
|
29
|
+
* - Typical config (1KB): ~0.5ms per read
|
|
30
|
+
* - Large config (50KB): ~1.6ms per read
|
|
31
|
+
* - Missing config: ~0.01ms per read
|
|
32
|
+
* Config is read 1-2 times per command (schema resolution + instruction loading),
|
|
33
|
+
* adding ~1-3ms total overhead. Caching would add complexity (mtime checks,
|
|
34
|
+
* invalidation logic) for negligible benefit. Direct reads also ensure config
|
|
35
|
+
* changes are reflected immediately without stale cache issues.
|
|
36
|
+
*
|
|
37
|
+
* @param projectRoot - The root directory of the project (where `openspec/` lives)
|
|
38
|
+
* @returns Parsed config or null if file doesn't exist
|
|
39
|
+
*/
|
|
40
|
+
export declare function readProjectConfig(projectRoot: string): ProjectConfig | null;
|
|
41
|
+
/**
|
|
42
|
+
* Validate artifact IDs in rules against a schema's artifacts.
|
|
43
|
+
* Called during instruction loading (when schema is known).
|
|
44
|
+
* Returns warnings for unknown artifact IDs.
|
|
45
|
+
*
|
|
46
|
+
* @param rules - The rules object from config
|
|
47
|
+
* @param validArtifactIds - Set of valid artifact IDs from the schema
|
|
48
|
+
* @param schemaName - Name of the schema for error messages
|
|
49
|
+
* @returns Array of warning messages for unknown artifact IDs
|
|
50
|
+
*/
|
|
51
|
+
export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>, schemaName: string): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Suggest valid schema names when user provides invalid schema.
|
|
54
|
+
* Uses fuzzy matching to find similar names.
|
|
55
|
+
*
|
|
56
|
+
* @param invalidSchemaName - The invalid schema name from config
|
|
57
|
+
* @param availableSchemas - List of available schemas with their type (built-in or project-local)
|
|
58
|
+
* @returns Error message with suggestions and available schemas
|
|
59
|
+
*/
|
|
60
|
+
export declare function suggestSchemas(invalidSchemaName: string, availableSchemas: {
|
|
61
|
+
name: string;
|
|
62
|
+
isBuiltIn: boolean;
|
|
63
|
+
}[]): string;
|
|
64
|
+
//# sourceMappingURL=project-config.d.ts.map
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
/**
|
|
6
|
+
* Zod schema for project configuration.
|
|
7
|
+
*
|
|
8
|
+
* Purpose:
|
|
9
|
+
* 1. Documentation - clearly defines the config file structure
|
|
10
|
+
* 2. Type safety - TypeScript infers ProjectConfig type from schema
|
|
11
|
+
* 3. Runtime validation - uses safeParse() for resilient field-by-field validation
|
|
12
|
+
*
|
|
13
|
+
* Why Zod over manual validation:
|
|
14
|
+
* - Helps understand OpenSpec's data interfaces at a glance
|
|
15
|
+
* - Single source of truth for type and validation
|
|
16
|
+
* - Consistent with other OpenSpec schemas
|
|
17
|
+
*/
|
|
18
|
+
export const ProjectConfigSchema = z.object({
|
|
19
|
+
// Required: which schema to use (e.g., "spec-driven", "tdd", or project-local schema name)
|
|
20
|
+
schema: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.describe('The workflow schema to use (e.g., "spec-driven", "tdd")'),
|
|
24
|
+
// Optional: project context (injected into all artifact instructions)
|
|
25
|
+
// Max size: 50KB (enforced during parsing)
|
|
26
|
+
context: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Project context injected into all artifact instructions'),
|
|
30
|
+
// Optional: per-artifact rules (additive to schema's built-in guidance)
|
|
31
|
+
rules: z
|
|
32
|
+
.record(z.string(), // artifact ID
|
|
33
|
+
z.array(z.string()) // list of rules
|
|
34
|
+
)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Per-artifact rules, keyed by artifact ID'),
|
|
37
|
+
});
|
|
38
|
+
const MAX_CONTEXT_SIZE = 50 * 1024; // 50KB hard limit
|
|
39
|
+
/**
|
|
40
|
+
* Read and parse openspec/config.yaml from project root.
|
|
41
|
+
* Uses resilient parsing - validates each field independently using Zod safeParse.
|
|
42
|
+
* Returns null if file doesn't exist.
|
|
43
|
+
* Returns partial config if some fields are invalid (with warnings).
|
|
44
|
+
*
|
|
45
|
+
* Performance note (Jan 2025):
|
|
46
|
+
* Benchmarks showed direct file reads are fast enough without caching:
|
|
47
|
+
* - Typical config (1KB): ~0.5ms per read
|
|
48
|
+
* - Large config (50KB): ~1.6ms per read
|
|
49
|
+
* - Missing config: ~0.01ms per read
|
|
50
|
+
* Config is read 1-2 times per command (schema resolution + instruction loading),
|
|
51
|
+
* adding ~1-3ms total overhead. Caching would add complexity (mtime checks,
|
|
52
|
+
* invalidation logic) for negligible benefit. Direct reads also ensure config
|
|
53
|
+
* changes are reflected immediately without stale cache issues.
|
|
54
|
+
*
|
|
55
|
+
* @param projectRoot - The root directory of the project (where `openspec/` lives)
|
|
56
|
+
* @returns Parsed config or null if file doesn't exist
|
|
57
|
+
*/
|
|
58
|
+
export function readProjectConfig(projectRoot) {
|
|
59
|
+
// Try both .yaml and .yml, prefer .yaml
|
|
60
|
+
let configPath = path.join(projectRoot, 'openspec', 'config.yaml');
|
|
61
|
+
if (!existsSync(configPath)) {
|
|
62
|
+
configPath = path.join(projectRoot, 'openspec', 'config.yml');
|
|
63
|
+
if (!existsSync(configPath)) {
|
|
64
|
+
return null; // No config is OK
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
69
|
+
const raw = parseYaml(content);
|
|
70
|
+
if (!raw || typeof raw !== 'object') {
|
|
71
|
+
console.warn(`openspec/config.yaml is not a valid YAML object`);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const config = {};
|
|
75
|
+
// Parse schema field using Zod
|
|
76
|
+
const schemaField = z.string().min(1);
|
|
77
|
+
const schemaResult = schemaField.safeParse(raw.schema);
|
|
78
|
+
if (schemaResult.success) {
|
|
79
|
+
config.schema = schemaResult.data;
|
|
80
|
+
}
|
|
81
|
+
else if (raw.schema !== undefined) {
|
|
82
|
+
console.warn(`Invalid 'schema' field in config (must be non-empty string)`);
|
|
83
|
+
}
|
|
84
|
+
// Parse context field with size limit
|
|
85
|
+
if (raw.context !== undefined) {
|
|
86
|
+
const contextField = z.string();
|
|
87
|
+
const contextResult = contextField.safeParse(raw.context);
|
|
88
|
+
if (contextResult.success) {
|
|
89
|
+
const contextSize = Buffer.byteLength(contextResult.data, 'utf-8');
|
|
90
|
+
if (contextSize > MAX_CONTEXT_SIZE) {
|
|
91
|
+
console.warn(`Context too large (${(contextSize / 1024).toFixed(1)}KB, limit: ${MAX_CONTEXT_SIZE / 1024}KB)`);
|
|
92
|
+
console.warn(`Ignoring context field`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
config.context = contextResult.data;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.warn(`Invalid 'context' field in config (must be string)`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Parse rules field using Zod
|
|
103
|
+
if (raw.rules !== undefined) {
|
|
104
|
+
const rulesField = z.record(z.string(), z.array(z.string()));
|
|
105
|
+
// First check if it's an object structure (guard against null since typeof null === 'object')
|
|
106
|
+
if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) {
|
|
107
|
+
const parsedRules = {};
|
|
108
|
+
let hasValidRules = false;
|
|
109
|
+
for (const [artifactId, rules] of Object.entries(raw.rules)) {
|
|
110
|
+
const rulesArrayResult = z.array(z.string()).safeParse(rules);
|
|
111
|
+
if (rulesArrayResult.success) {
|
|
112
|
+
// Filter out empty strings
|
|
113
|
+
const validRules = rulesArrayResult.data.filter((r) => r.length > 0);
|
|
114
|
+
if (validRules.length > 0) {
|
|
115
|
+
parsedRules[artifactId] = validRules;
|
|
116
|
+
hasValidRules = true;
|
|
117
|
+
}
|
|
118
|
+
if (validRules.length < rulesArrayResult.data.length) {
|
|
119
|
+
console.warn(`Some rules for '${artifactId}' are empty strings, ignoring them`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.warn(`Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (hasValidRules) {
|
|
127
|
+
config.rules = parsedRules;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.warn(`Invalid 'rules' field in config (must be object)`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Return partial config even if some fields failed
|
|
135
|
+
return Object.keys(config).length > 0 ? config : null;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.warn(`Failed to parse openspec/config.yaml:`, error);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Validate artifact IDs in rules against a schema's artifacts.
|
|
144
|
+
* Called during instruction loading (when schema is known).
|
|
145
|
+
* Returns warnings for unknown artifact IDs.
|
|
146
|
+
*
|
|
147
|
+
* @param rules - The rules object from config
|
|
148
|
+
* @param validArtifactIds - Set of valid artifact IDs from the schema
|
|
149
|
+
* @param schemaName - Name of the schema for error messages
|
|
150
|
+
* @returns Array of warning messages for unknown artifact IDs
|
|
151
|
+
*/
|
|
152
|
+
export function validateConfigRules(rules, validArtifactIds, schemaName) {
|
|
153
|
+
const warnings = [];
|
|
154
|
+
for (const artifactId of Object.keys(rules)) {
|
|
155
|
+
if (!validArtifactIds.has(artifactId)) {
|
|
156
|
+
const validIds = Array.from(validArtifactIds).sort().join(', ');
|
|
157
|
+
warnings.push(`Unknown artifact ID in rules: "${artifactId}". ` +
|
|
158
|
+
`Valid IDs for schema "${schemaName}": ${validIds}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return warnings;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Suggest valid schema names when user provides invalid schema.
|
|
165
|
+
* Uses fuzzy matching to find similar names.
|
|
166
|
+
*
|
|
167
|
+
* @param invalidSchemaName - The invalid schema name from config
|
|
168
|
+
* @param availableSchemas - List of available schemas with their type (built-in or project-local)
|
|
169
|
+
* @returns Error message with suggestions and available schemas
|
|
170
|
+
*/
|
|
171
|
+
export function suggestSchemas(invalidSchemaName, availableSchemas) {
|
|
172
|
+
// Simple fuzzy match: Levenshtein distance
|
|
173
|
+
function levenshtein(a, b) {
|
|
174
|
+
const matrix = [];
|
|
175
|
+
for (let i = 0; i <= b.length; i++) {
|
|
176
|
+
matrix[i] = [i];
|
|
177
|
+
}
|
|
178
|
+
for (let j = 0; j <= a.length; j++) {
|
|
179
|
+
matrix[0][j] = j;
|
|
180
|
+
}
|
|
181
|
+
for (let i = 1; i <= b.length; i++) {
|
|
182
|
+
for (let j = 1; j <= a.length; j++) {
|
|
183
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
184
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return matrix[b.length][a.length];
|
|
192
|
+
}
|
|
193
|
+
// Find closest matches (distance <= 3)
|
|
194
|
+
const suggestions = availableSchemas
|
|
195
|
+
.map((s) => ({ ...s, distance: levenshtein(invalidSchemaName, s.name) }))
|
|
196
|
+
.filter((s) => s.distance <= 3)
|
|
197
|
+
.sort((a, b) => a.distance - b.distance)
|
|
198
|
+
.slice(0, 3);
|
|
199
|
+
const builtIn = availableSchemas.filter((s) => s.isBuiltIn).map((s) => s.name);
|
|
200
|
+
const projectLocal = availableSchemas.filter((s) => !s.isBuiltIn).map((s) => s.name);
|
|
201
|
+
let message = `Schema '${invalidSchemaName}' not found in openspec/config.yaml\n\n`;
|
|
202
|
+
if (suggestions.length > 0) {
|
|
203
|
+
message += `Did you mean one of these?\n`;
|
|
204
|
+
suggestions.forEach((s) => {
|
|
205
|
+
const type = s.isBuiltIn ? 'built-in' : 'project-local';
|
|
206
|
+
message += ` - ${s.name} (${type})\n`;
|
|
207
|
+
});
|
|
208
|
+
message += '\n';
|
|
209
|
+
}
|
|
210
|
+
message += `Available schemas:\n`;
|
|
211
|
+
if (builtIn.length > 0) {
|
|
212
|
+
message += ` Built-in: ${builtIn.join(', ')}\n`;
|
|
213
|
+
}
|
|
214
|
+
if (projectLocal.length > 0) {
|
|
215
|
+
message += ` Project-local: ${projectLocal.join(', ')}\n`;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
message += ` Project-local: (none found)\n`;
|
|
219
|
+
}
|
|
220
|
+
message += `\nFix: Edit openspec/config.yaml and change 'schema: ${invalidSchemaName}' to a valid schema name`;
|
|
221
|
+
return message;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=project-config.js.map
|
|
@@ -11,33 +11,37 @@ export declare class ChangeMetadataError extends Error {
|
|
|
11
11
|
* Validates that a schema name is valid (exists in available schemas).
|
|
12
12
|
*
|
|
13
13
|
* @param schemaName - The schema name to validate
|
|
14
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
14
15
|
* @returns The validated schema name
|
|
15
16
|
* @throws Error if schema is not found
|
|
16
17
|
*/
|
|
17
|
-
export declare function validateSchemaName(schemaName: string): string;
|
|
18
|
+
export declare function validateSchemaName(schemaName: string, projectRoot?: string): string;
|
|
18
19
|
/**
|
|
19
20
|
* Writes change metadata to .openspec.yaml in the change directory.
|
|
20
21
|
*
|
|
21
22
|
* @param changeDir - The path to the change directory
|
|
22
23
|
* @param metadata - The metadata to write
|
|
24
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
23
25
|
* @throws ChangeMetadataError if validation fails or write fails
|
|
24
26
|
*/
|
|
25
|
-
export declare function writeChangeMetadata(changeDir: string, metadata: ChangeMetadata): void;
|
|
27
|
+
export declare function writeChangeMetadata(changeDir: string, metadata: ChangeMetadata, projectRoot?: string): void;
|
|
26
28
|
/**
|
|
27
29
|
* Reads change metadata from .openspec.yaml in the change directory.
|
|
28
30
|
*
|
|
29
31
|
* @param changeDir - The path to the change directory
|
|
32
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
30
33
|
* @returns The validated metadata, or null if no metadata file exists
|
|
31
34
|
* @throws ChangeMetadataError if the file exists but is invalid
|
|
32
35
|
*/
|
|
33
|
-
export declare function readChangeMetadata(changeDir: string): ChangeMetadata | null;
|
|
36
|
+
export declare function readChangeMetadata(changeDir: string, projectRoot?: string): ChangeMetadata | null;
|
|
34
37
|
/**
|
|
35
38
|
* Resolves the schema for a change, with explicit override taking precedence.
|
|
36
39
|
*
|
|
37
40
|
* Resolution order:
|
|
38
41
|
* 1. Explicit schema (if provided)
|
|
39
42
|
* 2. Schema from .openspec.yaml metadata (if exists)
|
|
40
|
-
* 3.
|
|
43
|
+
* 3. Schema from openspec/config.yaml (if exists)
|
|
44
|
+
* 4. Default 'spec-driven'
|
|
41
45
|
*
|
|
42
46
|
* @param changeDir - The path to the change directory
|
|
43
47
|
* @param explicitSchema - Optional explicit schema override
|
|
@@ -3,6 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
import * as yaml from 'yaml';
|
|
4
4
|
import { ChangeMetadataSchema } from '../core/artifact-graph/types.js';
|
|
5
5
|
import { listSchemas } from '../core/artifact-graph/resolver.js';
|
|
6
|
+
import { readProjectConfig } from '../core/project-config.js';
|
|
6
7
|
const METADATA_FILENAME = '.openspec.yaml';
|
|
7
8
|
/**
|
|
8
9
|
* Error thrown when change metadata validation fails.
|
|
@@ -21,11 +22,12 @@ export class ChangeMetadataError extends Error {
|
|
|
21
22
|
* Validates that a schema name is valid (exists in available schemas).
|
|
22
23
|
*
|
|
23
24
|
* @param schemaName - The schema name to validate
|
|
25
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
24
26
|
* @returns The validated schema name
|
|
25
27
|
* @throws Error if schema is not found
|
|
26
28
|
*/
|
|
27
|
-
export function validateSchemaName(schemaName) {
|
|
28
|
-
const availableSchemas = listSchemas();
|
|
29
|
+
export function validateSchemaName(schemaName, projectRoot) {
|
|
30
|
+
const availableSchemas = listSchemas(projectRoot);
|
|
29
31
|
if (!availableSchemas.includes(schemaName)) {
|
|
30
32
|
throw new Error(`Unknown schema '${schemaName}'. Available: ${availableSchemas.join(', ')}`);
|
|
31
33
|
}
|
|
@@ -36,12 +38,13 @@ export function validateSchemaName(schemaName) {
|
|
|
36
38
|
*
|
|
37
39
|
* @param changeDir - The path to the change directory
|
|
38
40
|
* @param metadata - The metadata to write
|
|
41
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
39
42
|
* @throws ChangeMetadataError if validation fails or write fails
|
|
40
43
|
*/
|
|
41
|
-
export function writeChangeMetadata(changeDir, metadata) {
|
|
44
|
+
export function writeChangeMetadata(changeDir, metadata, projectRoot) {
|
|
42
45
|
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
|
43
46
|
// Validate schema exists
|
|
44
|
-
validateSchemaName(metadata.schema);
|
|
47
|
+
validateSchemaName(metadata.schema, projectRoot);
|
|
45
48
|
// Validate with Zod
|
|
46
49
|
const parseResult = ChangeMetadataSchema.safeParse(metadata);
|
|
47
50
|
if (!parseResult.success) {
|
|
@@ -61,10 +64,11 @@ export function writeChangeMetadata(changeDir, metadata) {
|
|
|
61
64
|
* Reads change metadata from .openspec.yaml in the change directory.
|
|
62
65
|
*
|
|
63
66
|
* @param changeDir - The path to the change directory
|
|
67
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
64
68
|
* @returns The validated metadata, or null if no metadata file exists
|
|
65
69
|
* @throws ChangeMetadataError if the file exists but is invalid
|
|
66
70
|
*/
|
|
67
|
-
export function readChangeMetadata(changeDir) {
|
|
71
|
+
export function readChangeMetadata(changeDir, projectRoot) {
|
|
68
72
|
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
|
69
73
|
if (!fs.existsSync(metaPath)) {
|
|
70
74
|
return null;
|
|
@@ -91,7 +95,7 @@ export function readChangeMetadata(changeDir) {
|
|
|
91
95
|
throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath);
|
|
92
96
|
}
|
|
93
97
|
// Validate that the schema exists
|
|
94
|
-
const availableSchemas = listSchemas();
|
|
98
|
+
const availableSchemas = listSchemas(projectRoot);
|
|
95
99
|
if (!availableSchemas.includes(parseResult.data.schema)) {
|
|
96
100
|
throw new ChangeMetadataError(`Unknown schema '${parseResult.data.schema}'. Available: ${availableSchemas.join(', ')}`, metaPath);
|
|
97
101
|
}
|
|
@@ -103,28 +107,41 @@ export function readChangeMetadata(changeDir) {
|
|
|
103
107
|
* Resolution order:
|
|
104
108
|
* 1. Explicit schema (if provided)
|
|
105
109
|
* 2. Schema from .openspec.yaml metadata (if exists)
|
|
106
|
-
* 3.
|
|
110
|
+
* 3. Schema from openspec/config.yaml (if exists)
|
|
111
|
+
* 4. Default 'spec-driven'
|
|
107
112
|
*
|
|
108
113
|
* @param changeDir - The path to the change directory
|
|
109
114
|
* @param explicitSchema - Optional explicit schema override
|
|
110
115
|
* @returns The resolved schema name
|
|
111
116
|
*/
|
|
112
117
|
export function resolveSchemaForChange(changeDir, explicitSchema) {
|
|
118
|
+
// Derive project root from changeDir (changeDir is typically projectRoot/openspec/changes/change-name)
|
|
119
|
+
const projectRoot = path.resolve(changeDir, '../../..');
|
|
113
120
|
// 1. Explicit override wins
|
|
114
121
|
if (explicitSchema) {
|
|
115
122
|
return explicitSchema;
|
|
116
123
|
}
|
|
117
124
|
// 2. Try reading from metadata
|
|
118
125
|
try {
|
|
119
|
-
const metadata = readChangeMetadata(changeDir);
|
|
126
|
+
const metadata = readChangeMetadata(changeDir, projectRoot);
|
|
120
127
|
if (metadata?.schema) {
|
|
121
128
|
return metadata.schema;
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
catch {
|
|
125
|
-
// If metadata read fails,
|
|
132
|
+
// If metadata read fails, continue to next option
|
|
126
133
|
}
|
|
127
|
-
// 3.
|
|
134
|
+
// 3. Try reading from project config
|
|
135
|
+
try {
|
|
136
|
+
const config = readProjectConfig(projectRoot);
|
|
137
|
+
if (config?.schema) {
|
|
138
|
+
return config.schema;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// If config read fails, fall back to default
|
|
143
|
+
}
|
|
144
|
+
// 4. Default
|
|
128
145
|
return 'spec-driven';
|
|
129
146
|
}
|
|
130
147
|
//# sourceMappingURL=change-metadata.js.map
|
|
@@ -5,6 +5,13 @@ export interface CreateChangeOptions {
|
|
|
5
5
|
/** The workflow schema to use (default: 'spec-driven') */
|
|
6
6
|
schema?: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Result of creating a change.
|
|
10
|
+
*/
|
|
11
|
+
export interface CreateChangeResult {
|
|
12
|
+
/** The schema that was actually used (resolved from options, config, or default) */
|
|
13
|
+
schema: string;
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* Result of validating a change name.
|
|
10
17
|
*/
|
|
@@ -39,13 +46,17 @@ export declare function validateChangeName(name: string): ValidationResult;
|
|
|
39
46
|
* @throws Error if the schema name is invalid
|
|
40
47
|
* @throws Error if the change directory already exists
|
|
41
48
|
*
|
|
49
|
+
* @returns Result containing the resolved schema name
|
|
50
|
+
*
|
|
42
51
|
* @example
|
|
43
52
|
* // Creates openspec/changes/add-auth/ with default schema
|
|
44
|
-
* await createChange('/path/to/project', 'add-auth')
|
|
53
|
+
* const result = await createChange('/path/to/project', 'add-auth')
|
|
54
|
+
* console.log(result.schema) // 'spec-driven' or value from config
|
|
45
55
|
*
|
|
46
56
|
* @example
|
|
47
57
|
* // Creates openspec/changes/add-auth/ with TDD schema
|
|
48
|
-
* await createChange('/path/to/project', 'add-auth', { schema: 'tdd' })
|
|
58
|
+
* const result = await createChange('/path/to/project', 'add-auth', { schema: 'tdd' })
|
|
59
|
+
* console.log(result.schema) // 'tdd'
|
|
49
60
|
*/
|
|
50
|
-
export declare function createChange(projectRoot: string, name: string, options?: CreateChangeOptions): Promise<
|
|
61
|
+
export declare function createChange(projectRoot: string, name: string, options?: CreateChangeOptions): Promise<CreateChangeResult>;
|
|
51
62
|
//# sourceMappingURL=change-utils.d.ts.map
|