mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
fix(archive): never let a carried Purpose abort the archive
Self-review found a regression introduced by the carry-over: a delta whose `## Purpose` body contains a `### Requirement:` header put that header outside `## Requirements` in the new main spec, so the structure guard rejected it and archive exited 1. The same delta archived fine before this branch. Fall back to the placeholder and warn when the carried Purpose would make the new spec structurally invalid, so archive completes as it did before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2ccbf32a84
commit
651b42c6eb
@@ -2,4 +2,4 @@
|
||||
'@fission-ai/openspec': patch
|
||||
---
|
||||
|
||||
`openspec archive` now carries a delta spec's `## Purpose` into the main spec it creates for a brand-new capability, instead of always writing the `TBD - created by archiving change <name>. Update Purpose after archive.` placeholder over it. The section body is copied verbatim, fenced code blocks included. The placeholder still appears when the delta has no `## Purpose` header outside a code fence, or when the section body is empty, and the Purpose of an existing main spec is never touched.
|
||||
`openspec archive` now carries a delta spec's `## Purpose` into the main spec it creates for a brand-new capability, instead of always writing the `TBD - created by archiving change <name>. Update Purpose after archive.` placeholder over it. The section body is copied verbatim, fenced code blocks included. The placeholder still appears when the delta has no `## Purpose` header outside a code fence, when the section body is empty, or when carrying the body over would put a requirement header outside `## Requirements` (that last case warns and still completes the archive). The Purpose of an existing main spec is never touched.
|
||||
|
||||
@@ -103,6 +103,13 @@ Before moving the change to archive, the command SHALL apply delta changes to ma
|
||||
- **AND** the delta spec has no `## Purpose` header outside a fenced code block, or the section body is empty
|
||||
- **THEN** write the TBD placeholder Purpose naming the change to update after archive
|
||||
|
||||
#### Scenario: Delta Purpose that would invalidate the new main spec
|
||||
|
||||
- **WHEN** a delta creates a main spec that does not exist yet
|
||||
- **AND** carrying its `## Purpose` body over would place a requirement header outside the `## Requirements` section
|
||||
- **THEN** write the TBD placeholder Purpose instead and warn that the delta Purpose was ignored
|
||||
- **AND** complete the archive rather than aborting it
|
||||
|
||||
### Requirement: Confirmation Behavior
|
||||
|
||||
The spec update confirmation SHALL provide clear visibility into changes before they are applied.
|
||||
|
||||
@@ -223,6 +223,19 @@ export async function buildUpdatedSpec(
|
||||
}
|
||||
isNewSpec = true;
|
||||
targetContent = buildSpecSkeleton(specName, changeName, extractPurposeSection(changeContent));
|
||||
// A carried Purpose that hides a requirement header would make the new main
|
||||
// spec structurally invalid and abort an archive that succeeded before
|
||||
// #1413. Keep the placeholder rather than turning a warning into a failure.
|
||||
if (findMainSpecStructureIssues(targetContent).length > 0) {
|
||||
targetContent = buildSpecSkeleton(specName, changeName);
|
||||
if (!options.silent) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ Warning: ${specName} - delta Purpose ignored (it contains a requirement header); wrote the placeholder Purpose instead.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const structureIssues = findMainSpecStructureIssues(targetContent);
|
||||
|
||||
@@ -635,6 +635,49 @@ The system SHALL send a notification.
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to the placeholder when the delta Purpose hides a requirement header (issue #1413)', async () => {
|
||||
const changeName = 'new-spec-with-stray-header-in-purpose';
|
||||
const changeSpecDir = path.join(tempDir, 'openspec', 'changes', changeName, 'specs', 'widgets');
|
||||
await fs.mkdir(changeSpecDir, { recursive: true });
|
||||
|
||||
// A delta an agent can plausibly emit. Carrying this Purpose verbatim
|
||||
// would put a requirement header outside ## Requirements and abort the
|
||||
// archive - which succeeded before the Purpose carry-over existed.
|
||||
const specContent = `## Purpose
|
||||
|
||||
Handles widgets.
|
||||
|
||||
### Requirement: Stray header
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Real Requirement
|
||||
The system SHALL handle widgets.
|
||||
|
||||
#### Scenario: Widget handled
|
||||
- **WHEN** a widget arrives
|
||||
- **THEN** it is handled
|
||||
`;
|
||||
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
|
||||
|
||||
await archiveCommand.execute(changeName, { yes: true, noValidate: true });
|
||||
|
||||
const mainSpecPath = path.join(tempDir, 'openspec', 'specs', 'widgets', 'spec.md');
|
||||
const updatedContent = await fs.readFile(mainSpecPath, 'utf-8');
|
||||
expect(updatedContent).toContain(
|
||||
`TBD - created by archiving change ${changeName}. Update Purpose after archive.`
|
||||
);
|
||||
expect(updatedContent).not.toContain('### Requirement: Stray header');
|
||||
expect(updatedContent).toContain('### Requirement: Real Requirement');
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`Warning: widgets - delta Purpose ignored (it contains a requirement header)`)
|
||||
);
|
||||
|
||||
// The archive still completed rather than aborting.
|
||||
const archives = await fs.readdir(path.join(tempDir, 'openspec', 'changes', 'archive'));
|
||||
expect(archives.some(a => a.includes(changeName))).toBe(true);
|
||||
});
|
||||
|
||||
it('should not overwrite the Purpose of an existing main spec (issue #1413)', async () => {
|
||||
const changeName = 'existing-spec-with-purpose';
|
||||
const changeSpecDir = path.join(tempDir, 'openspec', 'changes', changeName, 'specs', 'billing');
|
||||
|
||||
Reference in New Issue
Block a user