mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
fix(init): guide project.md migration (#1999)
* feat(init): offer project.md migration * fix(config): preserve context newline state * test(init): prove migration preserves files * docs(setup): document project.md migration * fix(init): guide project.md migration * fix(init): cover migration destinations
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@fission-ai/openspec': patch
|
||||
---
|
||||
|
||||
Guide users through an AI-assisted migration from legacy `project.md` to `config.yaml`.
|
||||
@@ -34,6 +34,22 @@ Re-running init is safe:
|
||||
- Running init again with a new tool selected adds that tool.
|
||||
- The `--tools` flag skips the picker ([CLI reference](../reference/cli.md)).
|
||||
|
||||
### Migrate an existing `project.md`
|
||||
|
||||
Init does not copy legacy `openspec/project.md` into `config.yaml`. It keeps the file and prints an AI-assisted migration request.
|
||||
|
||||
In your AI chat:
|
||||
|
||||
```
|
||||
Review openspec/project.md and migrate its useful content to openspec/config.yaml.
|
||||
Keep context concise: include only project-wide facts needed during artifact creation, apply, and archive.
|
||||
Move artifact-specific guidance into rules for the matching artifacts.
|
||||
Move guidance for apply or archive into the matching operations entry.
|
||||
Leave out generic, outdated, or verbose material. Do not delete project.md.
|
||||
```
|
||||
|
||||
Review `config.yaml`, then delete `project.md` when ready.
|
||||
|
||||
## What init installs
|
||||
|
||||
Running init creates two things in your project:
|
||||
|
||||
@@ -1185,11 +1185,15 @@ export function formatProjectMdMigrationHint(): string {
|
||||
lines.push(' • openspec/project.md');
|
||||
lines.push(chalk.dim(' We won\'t delete this file. It may contain useful project context.'));
|
||||
lines.push('');
|
||||
lines.push(chalk.dim(' The new openspec/config.yaml has a "context:" section for planning'));
|
||||
lines.push(chalk.dim(' context. This is included in every OpenSpec request and works more'));
|
||||
lines.push(chalk.dim(' reliably than the old project.md approach.'));
|
||||
lines.push(chalk.dim(' Ask your AI assistant:'));
|
||||
lines.push('');
|
||||
lines.push(chalk.dim(' Review project.md, move any useful content to config.yaml\'s context'));
|
||||
lines.push(chalk.dim(' section, then delete the file when ready.'));
|
||||
lines.push(chalk.dim(' Review openspec/project.md and migrate its useful content to'));
|
||||
lines.push(chalk.dim(' openspec/config.yaml. Keep context concise: include only project-wide'));
|
||||
lines.push(chalk.dim(' facts needed during artifact creation, apply, and archive. Move'));
|
||||
lines.push(chalk.dim(' artifact-specific guidance into rules for the matching artifacts.'));
|
||||
lines.push(chalk.dim(' Move guidance for apply or archive into the matching operations entry.'));
|
||||
lines.push(chalk.dim(' Leave out generic, outdated, or verbose material. Do not delete project.md.'));
|
||||
lines.push('');
|
||||
lines.push(chalk.dim(' Review config.yaml, then delete project.md when ready.'));
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -156,6 +156,22 @@ describe('InitCommand', () => {
|
||||
expect(content).toContain('schema: spec-driven');
|
||||
});
|
||||
|
||||
it('should guide project.md migration without copying or deleting it', async () => {
|
||||
const openspecPath = path.join(testDir, 'openspec');
|
||||
const projectMdPath = path.join(openspecPath, 'project.md');
|
||||
await fs.mkdir(openspecPath, { recursive: true });
|
||||
await fs.writeFile(projectMdPath, '# Migrate me later\n');
|
||||
|
||||
await new InitCommand({ tools: 'none', force: true }).execute(testDir);
|
||||
|
||||
expect(readProjectConfig(testDir)?.context).toBeUndefined();
|
||||
expect(await fs.readFile(projectMdPath, 'utf-8')).toBe('# Migrate me later\n');
|
||||
expect(vi.mocked(console.log).mock.calls.flat().join('\n')).toContain(
|
||||
'Ask your AI assistant'
|
||||
);
|
||||
expect(confirmMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add the requested artifact language to a new config', async () => {
|
||||
const initCommand = new InitCommand({
|
||||
tools: 'none',
|
||||
|
||||
@@ -1046,7 +1046,8 @@ ${OPENSPEC_MARKERS.end}`);
|
||||
expect(summary).toContain('• openspec/project.md');
|
||||
expect(summary).toContain('won\'t delete this file');
|
||||
expect(summary).toContain('config.yaml');
|
||||
expect(summary).toContain('"context:"');
|
||||
expect(summary).toContain('Ask your AI assistant');
|
||||
expect(summary).toContain('rules for the matching artifacts');
|
||||
});
|
||||
|
||||
it('should include attention section with other legacy artifacts', () => {
|
||||
@@ -1148,19 +1149,26 @@ ${OPENSPEC_MARKERS.end}`);
|
||||
expect(hint).toContain('openspec/project.md');
|
||||
expect(hint).toContain('won\'t delete this file');
|
||||
expect(hint).toContain('config.yaml');
|
||||
expect(hint).toContain('"context:"');
|
||||
expect(hint).toContain('Ask your AI assistant');
|
||||
});
|
||||
|
||||
it('should include actionable instructions', () => {
|
||||
it('should include a pasteable AI-assisted migration request', () => {
|
||||
const hint = formatProjectMdMigrationHint();
|
||||
expect(hint).toContain('move any useful content');
|
||||
expect(hint).toContain('delete the file when ready');
|
||||
expect(hint).toContain('Review openspec/project.md');
|
||||
expect(hint).toContain('migrate its useful content to');
|
||||
expect(hint).toContain('Do not delete project.md');
|
||||
expect(hint).toContain('Review config.yaml, then delete project.md when ready');
|
||||
});
|
||||
|
||||
it('should explain the new context section benefits', () => {
|
||||
it('should guide the agent to distill and route the content', () => {
|
||||
const hint = formatProjectMdMigrationHint();
|
||||
expect(hint).toContain('included in every OpenSpec request');
|
||||
expect(hint).toContain('reliably');
|
||||
expect(hint).toContain('Keep context concise');
|
||||
expect(hint).toContain('only project-wide');
|
||||
expect(hint).toContain('artifact creation, apply, and archive');
|
||||
expect(hint).toContain('rules for the matching artifacts');
|
||||
expect(hint).toContain('matching operations entry');
|
||||
expect(hint).toContain('Leave out generic');
|
||||
expect(hint).toContain('outdated, or verbose material');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { claudeAdapter } from '../src/core/command-generation/adapters/claude.js';
|
||||
import { AI_TOOLS } from '../src/core/config.js';
|
||||
import { formatProjectMdMigrationHint } from '../src/core/legacy-cleanup.js';
|
||||
import { CORE_WORKFLOWS } from '../src/core/profiles.js';
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
@@ -20,6 +21,29 @@ const CORE_SECTION = PROFILES.split('## The core set')[1].split(
|
||||
)[0];
|
||||
|
||||
describe('setup documentation', () => {
|
||||
it('matches the AI-assisted project.md migration guidance', () => {
|
||||
const hint = formatProjectMdMigrationHint();
|
||||
const claims = [
|
||||
'Review openspec/project.md and migrate its useful content to',
|
||||
'Keep context concise',
|
||||
'only project-wide',
|
||||
'artifact creation, apply, and archive',
|
||||
'rules for the matching artifacts',
|
||||
'matching operations entry',
|
||||
'Leave out generic',
|
||||
'outdated, or verbose material',
|
||||
'Do not delete project.md',
|
||||
];
|
||||
|
||||
expect(SETUP).toContain('Init does not copy legacy `openspec/project.md`');
|
||||
for (const claim of claims) {
|
||||
expect(hint).toContain(claim);
|
||||
expect(SETUP).toContain(claim);
|
||||
}
|
||||
expect(hint).toContain('Review config.yaml, then delete project.md when ready.');
|
||||
expect(SETUP).toContain('Review `config.yaml`, then delete `project.md` when ready.');
|
||||
});
|
||||
|
||||
it('keeps the Claude Code paths and recovery commands aligned with OpenSpec', () => {
|
||||
const claude = AI_TOOLS.find((tool) => tool.value === 'claude');
|
||||
const claudeCommandPath = claudeAdapter.getFilePath('<id>').split(path.sep).join('/');
|
||||
|
||||
Reference in New Issue
Block a user