mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-12 05:00:36 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ced2a8791 | ||
|
|
a79b8b5c03 |
@@ -43,8 +43,6 @@ The command SHALL provide clear feedback when no items are present for the selec
|
||||
- **WHEN** no specs directory exists or contains no capabilities
|
||||
- **THEN** display: "No specs found."
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Flags
|
||||
The command SHALL accept flags to select the noun being listed.
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: List Command Behavior
|
||||
### Requirement: Command Execution
|
||||
|
||||
The current `list` command behavior SHALL be preserved but marked as deprecated.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
## MODIFIED Requirements
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Diff Command Enhancement
|
||||
|
||||
+2
@@ -24,6 +24,8 @@ Before moving the change to archive, the command SHALL apply delta changes to ma
|
||||
- **THEN** abort with error message showing the conflict
|
||||
- **AND** suggest manual resolution
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Display Output
|
||||
|
||||
The command SHALL provide clear feedback about delta operations.
|
||||
+2
@@ -31,6 +31,8 @@ The command SHALL show a requirement-level comparison displaying only changed re
|
||||
- Indicates removed requirements (not in future)
|
||||
- Aligns modified requirements for easy comparison
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Validation
|
||||
|
||||
The command SHALL validate that changes can be applied successfully.
|
||||
+2
-18
@@ -1,6 +1,6 @@
|
||||
# OpenSpec Conventions - Changes
|
||||
|
||||
## ADDED Requirements
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Header-Based Requirement Identification
|
||||
|
||||
@@ -31,8 +31,6 @@ Requirement headers SHALL serve as unique identifiers for programmatic matching
|
||||
- **THEN** ensure no duplicate headers exist within a spec
|
||||
- **AND** validation tools SHALL flag duplicate headers as errors
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Change Storage Convention
|
||||
|
||||
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
|
||||
@@ -100,18 +98,4 @@ The archive process SHALL programmatically apply delta changes to current specif
|
||||
- **AND** require manual resolution before proceeding
|
||||
- **AND** provide clear guidance on resolving conflicts
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Future State Storage
|
||||
|
||||
The system SHALL no longer store complete future-state specifications in change proposals.
|
||||
|
||||
**Reason for removal**: Replaced by delta-based change storage which provides better review experience and clearer change tracking.
|
||||
|
||||
**Migration path**: All new changes must use delta format.
|
||||
|
||||
#### Scenario: Deprecate future state storage
|
||||
|
||||
- **WHEN** creating a new change proposal
|
||||
- **THEN** do not include full future-state specs
|
||||
- **AND** include only ADDED/MODIFIED/REMOVED/RENAMED requirements under the change's `specs/` directory
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Delta: CLI List Command
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Command Execution
|
||||
The command SHALL scan and analyze either active changes or specs based on the selected mode.
|
||||
|
||||
#### Scenario: Scanning for changes (default)
|
||||
- **WHEN** `openspec list` is executed without flags
|
||||
- **THEN** scan the `openspec/changes/` directory for change directories
|
||||
- **AND** exclude the `archive/` subdirectory from results
|
||||
- **AND** parse each change's `tasks.md` file to count task completion
|
||||
|
||||
#### Scenario: Scanning for specs
|
||||
- **WHEN** `openspec list --specs` is executed
|
||||
- **THEN** scan the `openspec/specs/` directory for capabilities
|
||||
- **AND** read each capability's `spec.md`
|
||||
- **AND** parse requirements to compute requirement counts
|
||||
|
||||
### Requirement: Output Format
|
||||
The command SHALL display items in a clear, readable table format with mode-appropriate progress or counts.
|
||||
|
||||
#### Scenario: Displaying change list (default)
|
||||
- **WHEN** displaying the list of changes
|
||||
- **THEN** show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
|
||||
#### Scenario: Displaying spec list
|
||||
- **WHEN** displaying the list of specs
|
||||
- **THEN** show a table with columns:
|
||||
- Spec id (directory name)
|
||||
- Requirement count (e.g., "requirements 12")
|
||||
|
||||
### Requirement: Empty State
|
||||
The command SHALL provide clear feedback when no items are present for the selected mode.
|
||||
|
||||
#### Scenario: Handling empty state (changes)
|
||||
- **WHEN** no active changes exist (only archive/ or empty changes/)
|
||||
- **THEN** display: "No active changes found."
|
||||
|
||||
#### Scenario: Handling empty state (specs)
|
||||
- **WHEN** no specs directory exists or contains no capabilities
|
||||
- **THEN** display: "No specs found."
|
||||
|
||||
### Requirement: Flags
|
||||
The command SHALL accept flags to select the noun being listed.
|
||||
|
||||
#### Scenario: Selecting specs
|
||||
- **WHEN** `--specs` is provided
|
||||
- **THEN** list specs instead of changes
|
||||
|
||||
#### Scenario: Selecting changes
|
||||
- **WHEN** `--changes` is provided
|
||||
- **THEN** list changes explicitly (same as default behavior)
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ openspec archive [change-name] [--yes|-y]
|
||||
|
||||
Options:
|
||||
- `--yes`, `-y`: Skip confirmation prompts (for automation)
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Change Selection
|
||||
|
||||
The command SHALL support both interactive and direct change selection methods.
|
||||
@@ -72,26 +70,25 @@ The archive operation SHALL follow a structured process to safely move changes t
|
||||
|
||||
### Requirement: Spec Update Process
|
||||
|
||||
Before moving the change to archive, the command SHALL update main specs to reflect the deployed reality.
|
||||
Before moving the change to archive, the command SHALL apply delta changes to main specs to reflect the deployed reality.
|
||||
|
||||
#### Scenario: Updating specs from change
|
||||
#### Scenario: Applying delta changes
|
||||
|
||||
- **WHEN** the change contains specs in `changes/[name]/specs/`
|
||||
- **THEN** execute these steps:
|
||||
1. Analyze which specs will be affected by comparing with existing specs
|
||||
2. Display a summary of spec updates to the user (see Confirmation Behavior below)
|
||||
3. Prompt for confirmation unless `--yes` flag is provided
|
||||
4. If confirmed, for each capability spec in the change directory:
|
||||
- Copy the spec from `changes/[name]/specs/[capability]/spec.md` to `openspec/specs/[capability]/spec.md`
|
||||
- Create the target directory structure if it doesn't exist
|
||||
- Overwrite existing spec files (specs represent current reality, change specs are the new reality)
|
||||
- Track which specs were updated for the success message
|
||||
- **WHEN** archiving a change with delta-based specs
|
||||
- **THEN** parse and apply delta changes as defined in openspec-conventions
|
||||
- **AND** validate all operations before applying
|
||||
|
||||
#### Scenario: No specs in change
|
||||
#### Scenario: Validating delta changes
|
||||
|
||||
- **WHEN** no specs exist in the change
|
||||
- **THEN** skip the spec update step
|
||||
- **AND** proceed with archiving
|
||||
- **WHEN** processing delta changes
|
||||
- **THEN** perform validations as specified in openspec-conventions
|
||||
- **AND** if validation fails, show specific errors and abort
|
||||
|
||||
#### Scenario: Conflict detection
|
||||
|
||||
- **WHEN** applying deltas would create duplicate requirement headers
|
||||
- **THEN** abort with error message showing the conflict
|
||||
- **AND** suggest manual resolution
|
||||
|
||||
### Requirement: Confirmation Behavior
|
||||
|
||||
@@ -142,6 +139,66 @@ The command SHALL handle various error conditions gracefully.
|
||||
- Archive target already exists
|
||||
- File system permissions issues
|
||||
|
||||
### Requirement: Skip Specs Option
|
||||
|
||||
The archive command SHALL support a `--skip-specs` flag that skips all spec update operations and proceeds directly to archiving.
|
||||
|
||||
#### Scenario: Skipping spec updates with flag
|
||||
|
||||
- **WHEN** executing `openspec archive <change> --skip-specs`
|
||||
- **THEN** skip spec discovery and update confirmation
|
||||
- **AND** proceed directly to moving the change to archive
|
||||
- **AND** display a message indicating specs were skipped
|
||||
|
||||
### Requirement: Non-blocking confirmation
|
||||
|
||||
The archive operation SHALL proceed when the user declines spec updates instead of cancelling the entire operation.
|
||||
|
||||
#### Scenario: User declines spec update confirmation
|
||||
|
||||
- **WHEN** the user declines spec update confirmation
|
||||
- **THEN** skip spec updates
|
||||
- **AND** continue with the archive operation
|
||||
- **AND** display a success message indicating specs were not updated
|
||||
|
||||
### Requirement: Display Output
|
||||
|
||||
The command SHALL provide clear feedback about delta operations.
|
||||
|
||||
#### Scenario: Showing delta application
|
||||
|
||||
- **WHEN** applying delta changes
|
||||
- **THEN** display for each spec:
|
||||
- Number of requirements added
|
||||
- Number of requirements modified
|
||||
- Number of requirements removed
|
||||
- Number of requirements renamed
|
||||
- **AND** use standard output symbols (+ ~ - →) as defined in openspec-conventions:
|
||||
```
|
||||
Applying changes to specs/user-auth/spec.md:
|
||||
+ 2 added
|
||||
~ 3 modified
|
||||
- 1 removed
|
||||
→ 1 renamed
|
||||
```
|
||||
|
||||
### Requirement: Archive Validation
|
||||
|
||||
The archive command SHALL validate changes before applying them to ensure data integrity.
|
||||
|
||||
#### Scenario: Pre-archive validation
|
||||
|
||||
- **WHEN** executing `openspec archive change-name`
|
||||
- **THEN** validate the change structure first
|
||||
- **AND** only proceed if validation passes
|
||||
- **AND** show validation errors if it fails
|
||||
|
||||
#### Scenario: Force archive without validation
|
||||
|
||||
- **WHEN** executing `openspec archive change-name --no-validate`
|
||||
- **THEN** skip validation (unsafe mode)
|
||||
- **AND** show warning about skipping validation
|
||||
|
||||
## Why These Decisions
|
||||
|
||||
**Interactive selection**: Reduces typing and helps users see available changes
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# cli-change Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-change-commands. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Change Command
|
||||
|
||||
The system SHALL provide a `change` command with subcommands for displaying, listing, and validating change proposals.
|
||||
|
||||
#### Scenario: Show change as JSON
|
||||
|
||||
- **WHEN** executing `openspec change show update-error --json`
|
||||
- **THEN** parse the markdown change file
|
||||
- **AND** extract change structure and deltas
|
||||
- **AND** output valid JSON to stdout
|
||||
|
||||
#### Scenario: List all changes
|
||||
|
||||
- **WHEN** executing `openspec change list`
|
||||
- **THEN** scan the openspec/changes directory
|
||||
- **AND** return list of all pending changes
|
||||
- **AND** support JSON output with `--json` flag
|
||||
|
||||
#### Scenario: Show only requirement changes
|
||||
|
||||
- **WHEN** executing `openspec change show update-error --requirements-only`
|
||||
- **THEN** display only the requirement changes (ADDED/MODIFIED/REMOVED/RENAMED)
|
||||
- **AND** exclude why and what changes sections
|
||||
|
||||
#### Scenario: Validate change structure
|
||||
|
||||
- **WHEN** executing `openspec change validate update-error`
|
||||
- **THEN** parse the change file
|
||||
- **AND** validate against Zod schema
|
||||
- **AND** ensure deltas are well-formed
|
||||
|
||||
### Requirement: Legacy Compatibility
|
||||
|
||||
The system SHALL maintain backward compatibility with the existing `list` command while showing deprecation notices.
|
||||
|
||||
#### Scenario: Legacy list command
|
||||
|
||||
- **WHEN** executing `openspec list`
|
||||
- **THEN** display current list of changes (existing behavior)
|
||||
- **AND** show deprecation notice: "Note: 'openspec list' is deprecated. Use 'openspec change list' instead."
|
||||
|
||||
#### Scenario: Legacy list with --all flag
|
||||
|
||||
- **WHEN** executing `openspec list --all`
|
||||
- **THEN** display all changes (existing behavior)
|
||||
- **AND** show same deprecation notice
|
||||
|
||||
### Requirement: Interactive show selection
|
||||
|
||||
The change show command SHALL support interactive selection when no change name is provided.
|
||||
|
||||
#### Scenario: Interactive change selection for show
|
||||
|
||||
- **WHEN** executing `openspec change show` without arguments
|
||||
- **THEN** display an interactive list of available changes
|
||||
- **AND** allow the user to select a change to show
|
||||
- **AND** display the selected change content
|
||||
- **AND** maintain all existing show options (--json, --deltas-only)
|
||||
|
||||
#### Scenario: Non-interactive fallback keeps current behavior
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec change show` without a change name
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print the existing hint including available change IDs
|
||||
- **AND** set `process.exitCode = 1`
|
||||
|
||||
### Requirement: Interactive validation selection
|
||||
|
||||
The change validate command SHALL support interactive selection when no change name is provided.
|
||||
|
||||
#### Scenario: Interactive change selection for validation
|
||||
|
||||
- **WHEN** executing `openspec change validate` without arguments
|
||||
- **THEN** display an interactive list of available changes
|
||||
- **AND** allow the user to select a change to validate
|
||||
- **AND** validate the selected change
|
||||
|
||||
#### Scenario: Non-interactive fallback keeps current behavior
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec change validate` without a change name
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print the existing hint including available change IDs
|
||||
- **AND** set `process.exitCode = 1`
|
||||
|
||||
@@ -9,9 +9,7 @@ The `openspec diff` command provides developers with a visual comparison between
|
||||
```bash
|
||||
openspec diff [change-name]
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Without Arguments
|
||||
|
||||
The command SHALL provide an interactive selection when no change is specified.
|
||||
@@ -33,35 +31,18 @@ The command SHALL compare specs when a specific change is provided.
|
||||
|
||||
### Requirement: Diff Output
|
||||
|
||||
The command SHALL generate appropriate diff output for all spec changes.
|
||||
The command SHALL show a requirement-level comparison displaying only changed requirements.
|
||||
|
||||
#### Scenario: Comparing existing files
|
||||
#### Scenario: Side-by-side comparison of changes
|
||||
|
||||
- **WHEN** file exists in both locations
|
||||
- **THEN** show unified diff
|
||||
|
||||
#### Scenario: New files
|
||||
|
||||
- **WHEN** file only exists in change
|
||||
- **THEN** show as new file (all lines with +)
|
||||
|
||||
#### Scenario: Deleted files
|
||||
|
||||
- **WHEN** file only exists in current specs
|
||||
- **THEN** show as deleted (all lines with -)
|
||||
|
||||
### Requirement: Display Format
|
||||
|
||||
The command SHALL use standard unified diff format for consistency with existing tools.
|
||||
|
||||
#### Scenario: Formatting diff output
|
||||
|
||||
- **WHEN** displaying diff output
|
||||
- **THEN** use standard unified diff format:
|
||||
- Lines prefixed with `-` for removed content
|
||||
- Lines prefixed with `+` for added content
|
||||
- Lines without prefix for unchanged context
|
||||
- File headers showing the paths being compared
|
||||
- **WHEN** running `openspec diff <change>`
|
||||
- **THEN** display only requirements that have changed
|
||||
- **AND** show them in a side-by-side format that:
|
||||
- Clearly shows the current version on the left
|
||||
- Shows the future version on the right
|
||||
- Indicates new requirements (not in current)
|
||||
- Indicates removed requirements (not in future)
|
||||
- Aligns modified requirements for easy comparison
|
||||
|
||||
### Requirement: Color Support
|
||||
|
||||
@@ -95,6 +76,28 @@ The command SHALL provide clear error messages for various failure conditions.
|
||||
- **WHEN** changes directory doesn't exist
|
||||
- **THEN** display "No OpenSpec changes directory found"
|
||||
|
||||
### Requirement: Validation
|
||||
|
||||
The command SHALL validate that changes can be applied successfully.
|
||||
|
||||
#### Scenario: Invalid delta references
|
||||
|
||||
- **WHEN** delta references non-existent requirement
|
||||
- **THEN** show error message with specific requirement
|
||||
- **AND** continue showing other valid changes
|
||||
- **AND** clearly mark failed changes in the output
|
||||
|
||||
### Requirement: Diff Command Enhancement
|
||||
|
||||
The diff command SHALL validate change structure before displaying differences.
|
||||
|
||||
#### Scenario: Validate before diff
|
||||
|
||||
- **WHEN** executing `openspec diff change-name`
|
||||
- **THEN** validate change structure
|
||||
- **AND** show validation warnings if present
|
||||
- **AND** continue with diff display
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,26 +3,21 @@
|
||||
## Purpose
|
||||
|
||||
The `openspec list` command SHALL provide developers with a quick overview of all active changes in the project, showing their names and task completion status.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Command Execution
|
||||
The command SHALL scan and analyze either active changes or specs based on the selected mode.
|
||||
|
||||
The command SHALL scan and analyze items based on the selected mode to provide a comprehensive overview.
|
||||
|
||||
#### Scenario: Scanning for changes
|
||||
|
||||
- **WHEN** `openspec list` is executed (default)
|
||||
#### Scenario: Scanning for changes (default)
|
||||
- **WHEN** `openspec list` is executed without flags
|
||||
- **THEN** scan the `openspec/changes/` directory for change directories
|
||||
- **AND** exclude the `archive/` subdirectory from results
|
||||
- **AND** parse each change's `tasks.md` file to count task completion
|
||||
|
||||
#### Scenario: Scanning for specs
|
||||
|
||||
- **WHEN** `openspec list --specs` is executed
|
||||
- **THEN** scan the `openspec/specs/` directory for capability directories
|
||||
- **THEN** scan the `openspec/specs/` directory for capabilities
|
||||
- **AND** read each capability's `spec.md`
|
||||
- **AND** parse the requirements to compute requirement counts
|
||||
- **AND** parse requirements to compute requirement counts
|
||||
|
||||
### Requirement: Task Counting
|
||||
|
||||
@@ -37,61 +32,40 @@ The command SHALL accurately count task completion status using standard markdow
|
||||
- **AND** calculate total tasks as the sum of completed and incomplete
|
||||
|
||||
### Requirement: Output Format
|
||||
The command SHALL display items in a clear, readable table format with mode-appropriate progress or counts.
|
||||
|
||||
The command SHALL display items in a clear, readable table format with mode-appropriate indicators.
|
||||
|
||||
#### Scenario: Displaying change list
|
||||
|
||||
- **WHEN** displaying the list
|
||||
#### Scenario: Displaying change list (default)
|
||||
- **WHEN** displaying the list of changes
|
||||
- **THEN** show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
|
||||
#### Scenario: Displaying spec list
|
||||
|
||||
- **WHEN** displaying the list for specs mode
|
||||
- **WHEN** displaying the list of specs
|
||||
- **THEN** show a table with columns:
|
||||
- Spec id (directory name)
|
||||
- Requirement count (e.g., "requirements 12")
|
||||
- **AND** use status indicators:
|
||||
- `✓` for fully completed changes (all tasks done)
|
||||
- Progress fraction for partial completion
|
||||
|
||||
Example output:
|
||||
```
|
||||
Changes:
|
||||
add-auth-feature 3/5 tasks
|
||||
update-api-docs ✓ Complete
|
||||
fix-validation 0/2 tasks
|
||||
add-list-command 1/4 tasks
|
||||
```
|
||||
|
||||
### Requirement: Flags
|
||||
|
||||
The command SHALL accept flags to choose the noun being listed.
|
||||
The command SHALL accept flags to select the noun being listed.
|
||||
|
||||
#### Scenario: Selecting specs
|
||||
|
||||
- **WHEN** `--specs` is provided
|
||||
- **THEN** list specs instead of changes
|
||||
|
||||
#### Scenario: Selecting changes
|
||||
|
||||
- **WHEN** `--changes` is provided
|
||||
- **THEN** list changes explicitly (same as default behavior)
|
||||
|
||||
### Requirement: Empty State
|
||||
|
||||
The command SHALL provide clear feedback when no items are present for the selected mode.
|
||||
|
||||
#### Scenario: Handling empty state (changes)
|
||||
|
||||
- **WHEN** no active changes exist (only archive/ or empty changes/)
|
||||
- **THEN** display: "No active changes found."
|
||||
|
||||
#### Scenario: Handling empty state (specs)
|
||||
|
||||
- **WHEN** no specs exist or the `openspec/specs/` directory is missing
|
||||
- **WHEN** no specs directory exists or contains no capabilities
|
||||
- **THEN** display: "No specs found."
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# cli-show Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-interactive-show-command. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Top-level show command
|
||||
|
||||
The CLI SHALL provide a top-level `show` command for displaying changes and specs with intelligent selection.
|
||||
|
||||
#### Scenario: Interactive show selection
|
||||
|
||||
- **WHEN** executing `openspec show` without arguments
|
||||
- **THEN** prompt user to select type (change or spec)
|
||||
- **AND** display list of available items for selected type
|
||||
- **AND** show the selected item's content
|
||||
|
||||
#### Scenario: Non-interactive environments do not prompt
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec show` without arguments
|
||||
- **THEN** do not prompt
|
||||
- **AND** print a helpful hint with examples for `openspec show <item>` or `openspec change/spec show`
|
||||
- **AND** exit with code 1
|
||||
|
||||
#### Scenario: Direct item display
|
||||
|
||||
- **WHEN** executing `openspec show <item-name>`
|
||||
- **THEN** automatically detect if item is a change or spec
|
||||
- **AND** display the item's content
|
||||
- **AND** use appropriate formatting based on item type
|
||||
|
||||
#### Scenario: Type detection and ambiguity handling
|
||||
|
||||
- **WHEN** executing `openspec show <item-name>`
|
||||
- **THEN** if `<item-name>` uniquely matches a change or a spec, show that item
|
||||
- **AND** if it matches both, print an ambiguity error and suggest `--type change|spec` or using `openspec change show`/`openspec spec show`
|
||||
- **AND** if it matches neither, print not-found with nearest-match suggestions
|
||||
|
||||
#### Scenario: Explicit type override
|
||||
|
||||
- **WHEN** executing `openspec show --type change <item>`
|
||||
- **THEN** treat `<item>` as a change ID and show it (skipping auto-detection)
|
||||
|
||||
- **WHEN** executing `openspec show --type spec <item>`
|
||||
- **THEN** treat `<item>` as a spec ID and show it (skipping auto-detection)
|
||||
|
||||
### Requirement: Output format options
|
||||
|
||||
The show command SHALL support various output formats consistent with existing commands.
|
||||
|
||||
#### Scenario: JSON output
|
||||
|
||||
- **WHEN** executing `openspec show <item> --json`
|
||||
- **THEN** output the item in JSON format
|
||||
- **AND** include parsed metadata and structure
|
||||
- **AND** maintain format consistency with existing change/spec show commands
|
||||
|
||||
#### Scenario: Flag scoping and delegation
|
||||
|
||||
- **WHEN** showing a change or a spec via the top-level command
|
||||
- **THEN** accept common flags such as `--json`
|
||||
- **AND** pass through type-specific flags to the corresponding implementation
|
||||
- Change-only flags: `--deltas-only` (alias `--requirements-only` deprecated)
|
||||
- Spec-only flags: `--requirements`, `--no-scenarios`, `-r/--requirement`
|
||||
- **AND** ignore irrelevant flags for the detected type with a warning
|
||||
|
||||
### Requirement: Interactivity controls
|
||||
|
||||
- The CLI SHALL respect `--no-interactive` to disable prompts.
|
||||
- The CLI SHALL respect `OPEN_SPEC_INTERACTIVE=0` to disable prompts globally.
|
||||
- Interactive prompts SHALL only be shown when stdin is a TTY and interactivity is not disabled.
|
||||
|
||||
#### Scenario: Change-specific options
|
||||
|
||||
- **WHEN** showing a change with `openspec show <change-name> --deltas-only`
|
||||
- **THEN** display only the deltas in JSON format
|
||||
- **AND** maintain compatibility with existing change show options
|
||||
|
||||
#### Scenario: Spec-specific options
|
||||
|
||||
- **WHEN** showing a spec with `openspec show <spec-id> --requirements`
|
||||
- **THEN** display only requirements in JSON format
|
||||
- **AND** support other spec options (--no-scenarios, -r)
|
||||
- **AND** maintain compatibility with existing spec show options
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# cli-spec Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-interactive-show-command. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Interactive spec show
|
||||
|
||||
The spec show command SHALL support interactive selection when no spec-id is provided.
|
||||
|
||||
#### Scenario: Interactive spec selection for show
|
||||
|
||||
- **WHEN** executing `openspec spec show` without arguments
|
||||
- **THEN** display an interactive list of available specs
|
||||
- **AND** allow the user to select a spec to show
|
||||
- **AND** display the selected spec content
|
||||
- **AND** maintain all existing show options (--json, --requirements, --no-scenarios, -r)
|
||||
|
||||
#### Scenario: Non-interactive fallback keeps current behavior
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec spec show` without a spec-id
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print the existing error message for missing spec-id
|
||||
- **AND** set non-zero exit code
|
||||
|
||||
### Requirement: Spec Command
|
||||
|
||||
The system SHALL provide a `spec` command with subcommands for displaying, listing, and validating specifications.
|
||||
|
||||
#### Scenario: Show spec as JSON
|
||||
|
||||
- **WHEN** executing `openspec spec show init --json`
|
||||
- **THEN** parse the markdown spec file
|
||||
- **AND** extract headings and content hierarchically
|
||||
- **AND** output valid JSON to stdout
|
||||
|
||||
#### Scenario: List all specs
|
||||
|
||||
- **WHEN** executing `openspec spec list`
|
||||
- **THEN** scan the openspec/specs directory
|
||||
- **AND** return list of all available capabilities
|
||||
- **AND** support JSON output with `--json` flag
|
||||
|
||||
#### Scenario: Filter spec content
|
||||
|
||||
- **WHEN** executing `openspec spec show init --requirements`
|
||||
- **THEN** display only requirement names and SHALL statements
|
||||
- **AND** exclude scenario content
|
||||
|
||||
#### Scenario: Validate spec structure
|
||||
|
||||
- **WHEN** executing `openspec spec validate init`
|
||||
- **THEN** parse the spec file
|
||||
- **AND** validate against Zod schema
|
||||
- **AND** report any structural issues
|
||||
|
||||
### Requirement: JSON Schema Definition
|
||||
|
||||
The system SHALL define Zod schemas that accurately represent the spec structure for runtime validation.
|
||||
|
||||
#### Scenario: Schema validation
|
||||
|
||||
- **WHEN** parsing a spec into JSON
|
||||
- **THEN** validate the structure using Zod schemas
|
||||
- **AND** ensure all required fields are present
|
||||
- **AND** provide clear error messages for validation failures
|
||||
|
||||
### Requirement: Interactive spec validation
|
||||
|
||||
The spec validate command SHALL support interactive selection when no spec-id is provided.
|
||||
|
||||
#### Scenario: Interactive spec selection for validation
|
||||
|
||||
- **WHEN** executing `openspec spec validate` without arguments
|
||||
- **THEN** display an interactive list of available specs
|
||||
- **AND** allow the user to select a spec to validate
|
||||
- **AND** validate the selected spec
|
||||
- **AND** maintain all existing validation options (--strict, --json)
|
||||
|
||||
#### Scenario: Non-interactive fallback keeps current behavior
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec spec validate` without a spec-id
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print the existing error message for missing spec-id
|
||||
- **AND** set non-zero exit code
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
## Purpose
|
||||
|
||||
As a developer using OpenSpec, I want to update the OpenSpec instructions in my project when new versions are released, so that I can benefit from improvements to AI agent instructions.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Update Behavior
|
||||
|
||||
The update command SHALL update OpenSpec instruction files to the latest templates in a team-friendly manner.
|
||||
@@ -48,6 +46,28 @@ The update command SHALL handle file updates in a predictable and safe manner.
|
||||
- **AND** be idempotent (repeated runs have no additional effect)
|
||||
- **AND** respect team members' AI tool choices by not creating unwanted files
|
||||
|
||||
### Requirement: Tool-Agnostic Updates
|
||||
|
||||
The update command SHALL update only existing AI tool configuration files and SHALL NOT create new ones.
|
||||
|
||||
#### Scenario: Updating existing tool files
|
||||
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** update each AI tool configuration file that exists (e.g., CLAUDE.md, COPILOT.md)
|
||||
- **AND** do not create missing tool configuration files
|
||||
- **AND** preserve user content outside OpenSpec markers
|
||||
|
||||
### Requirement: Core Files Always Updated
|
||||
|
||||
The update command SHALL always update the core OpenSpec files and display an ASCII-safe success message.
|
||||
|
||||
#### Scenario: Successful update
|
||||
|
||||
- **WHEN** the update completes successfully
|
||||
- **THEN** replace `openspec/README.md` with the latest template
|
||||
- **AND** update existing AI tool configuration files within markers
|
||||
- **AND** display the message: "Updated OpenSpec instructions"
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# cli-validate Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change improve-validate-error-messages. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Validation SHALL provide actionable remediation steps
|
||||
Validation output SHALL include specific guidance to fix each error, including expected structure, example headers, and suggested commands to verify fixes.
|
||||
|
||||
#### Scenario: No deltas found in change
|
||||
- **WHEN** validating a change with zero parsed deltas
|
||||
- **THEN** show error "No deltas found" with guidance:
|
||||
- Ensure `openspec/changes/{id}/specs/` exists with `.md` files
|
||||
- Use delta headers: `## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`
|
||||
- Each requirement must include at least one `#### Scenario:` block
|
||||
- Try: `openspec change show {id} --json --deltas-only` to inspect what was parsed
|
||||
|
||||
#### Scenario: Missing required sections
|
||||
- **WHEN** a required section is missing
|
||||
- **THEN** the validator SHALL include expected header names and a minimal skeleton:
|
||||
- For Spec: `## Purpose`, `## Requirements`
|
||||
- For Change: `## Why`, `## What Changes`
|
||||
- Show an example snippet of the missing section
|
||||
|
||||
### Requirement: Validator SHALL detect likely misformatted scenarios and warn with a fix
|
||||
The validator SHALL recognize bulleted lines that look like scenarios (e.g., lines beginning with WHEN/THEN/AND) and emit a targeted warning with a conversion example to `#### Scenario:`.
|
||||
|
||||
#### Scenario: Bulleted WHEN/THEN under a Requirement
|
||||
- **WHEN** bullets that start with WHEN/THEN/AND are found under a requirement without any `#### Scenario:` headers
|
||||
- **THEN** emit warning: "Scenarios must use '#### Scenario:' headers", and show a conversion template:
|
||||
```
|
||||
#### Scenario: Short name
|
||||
- **WHEN** ...
|
||||
- **THEN** ...
|
||||
- **AND** ...
|
||||
```
|
||||
|
||||
### Requirement: All issues SHALL include file paths and structured locations
|
||||
Error, warning, and info messages SHALL include:
|
||||
- Source file path (`openspec/changes/{id}/proposal.md`, `.../specs/{cap}/spec.md`)
|
||||
- Structured path (e.g., `deltas[0].requirements[0].scenarios`)
|
||||
|
||||
#### Scenario: Zod validation error
|
||||
- **WHEN** a schema validation fails
|
||||
- **THEN** the message SHALL include `file`, `path`, and a remediation hint if applicable
|
||||
|
||||
### Requirement: Invalid results SHALL include a Next steps footer in human-readable output
|
||||
The CLI SHALL append a Next steps footer when the item is invalid and not using `--json`, including:
|
||||
- Summary line with counts
|
||||
- Top-3 guidance bullets (contextual to the most frequent or blocking errors)
|
||||
- A suggestion to re-run with `--json` and/or the debug command
|
||||
|
||||
#### Scenario: Change invalid summary
|
||||
- **WHEN** a change validation fails
|
||||
- **THEN** print "Next steps" with 2-3 targeted bullets and suggest `openspec change show <id> --json --deltas-only`
|
||||
|
||||
### Requirement: Top-level validate command
|
||||
|
||||
The CLI SHALL provide a top-level `validate` command for validating changes and specs with flexible selection options.
|
||||
|
||||
#### Scenario: Interactive validation selection
|
||||
|
||||
- **WHEN** executing `openspec validate` without arguments
|
||||
- **THEN** prompt user to select what to validate (all, changes, specs, or specific item)
|
||||
- **AND** perform validation based on selection
|
||||
- **AND** display results with appropriate formatting
|
||||
|
||||
#### Scenario: Non-interactive environments do not prompt
|
||||
|
||||
- **GIVEN** stdin is not a TTY or `--no-interactive` is provided or environment variable `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **WHEN** executing `openspec validate` without arguments
|
||||
- **THEN** do not prompt interactively
|
||||
- **AND** print a helpful hint listing available commands/flags and exit with code 1
|
||||
|
||||
#### Scenario: Direct item validation
|
||||
|
||||
- **WHEN** executing `openspec validate <item-name>`
|
||||
- **THEN** automatically detect if item is a change or spec
|
||||
- **AND** validate the specified item
|
||||
- **AND** display validation results
|
||||
|
||||
### Requirement: Bulk and filtered validation
|
||||
|
||||
The validate command SHALL support flags for bulk validation (--all) and filtered validation by type (--changes, --specs).
|
||||
|
||||
#### Scenario: Validate everything
|
||||
|
||||
- **WHEN** executing `openspec validate --all`
|
||||
- **THEN** validate all changes in openspec/changes/ (excluding archive)
|
||||
- **AND** validate all specs in openspec/specs/
|
||||
- **AND** display a summary showing passed/failed items
|
||||
- **AND** exit with code 1 if any validation fails
|
||||
|
||||
#### Scenario: Scope of bulk validation
|
||||
|
||||
- **WHEN** validating with `--all` or `--changes`
|
||||
- **THEN** include all change proposals under `openspec/changes/`
|
||||
- **AND** exclude the `openspec/changes/archive/` directory
|
||||
|
||||
- **WHEN** validating with `--specs`
|
||||
- **THEN** include all specs that have a `spec.md` under `openspec/specs/<id>/spec.md`
|
||||
|
||||
#### Scenario: Validate all changes
|
||||
|
||||
- **WHEN** executing `openspec validate --changes`
|
||||
- **THEN** validate all changes in openspec/changes/ (excluding archive)
|
||||
- **AND** display results for each change
|
||||
- **AND** show summary statistics
|
||||
|
||||
#### Scenario: Validate all specs
|
||||
|
||||
- **WHEN** executing `openspec validate --specs`
|
||||
- **THEN** validate all specs in openspec/specs/
|
||||
- **AND** display results for each spec
|
||||
- **AND** show summary statistics
|
||||
|
||||
### Requirement: Validation options and progress indication
|
||||
|
||||
The validate command SHALL support standard validation options (--strict, --json) and display progress during bulk operations.
|
||||
|
||||
#### Scenario: Strict validation
|
||||
|
||||
- **WHEN** executing `openspec validate --all --strict`
|
||||
- **THEN** apply strict validation to all items
|
||||
- **AND** treat warnings as errors
|
||||
- **AND** fail if any item has warnings or errors
|
||||
|
||||
#### Scenario: JSON output
|
||||
|
||||
- **WHEN** executing `openspec validate --all --json`
|
||||
- **THEN** output validation results as JSON
|
||||
- **AND** include detailed issues for each item
|
||||
- **AND** include summary statistics
|
||||
|
||||
#### Scenario: JSON output schema for bulk validation
|
||||
|
||||
- **WHEN** executing `openspec validate --all --json` (or `--changes` / `--specs`)
|
||||
- **THEN** output a JSON object with the following shape:
|
||||
- `items`: Array of objects with fields `{ id: string, type: "change"|"spec", valid: boolean, issues: Issue[], durationMs: number }`
|
||||
- `summary`: Object `{ totals: { items: number, passed: number, failed: number }, byType: { change?: { items: number, passed: number, failed: number }, spec?: { items: number, passed: number, failed: number } } }`
|
||||
- `version`: String identifier for the schema (e.g., `"1.0"`)
|
||||
- **AND** exit with code 1 if any `items[].valid === false`
|
||||
|
||||
Where `Issue` follows the existing per-item validation report shape `{ level: "ERROR"|"WARNING"|"INFO", path: string, message: string }`.
|
||||
|
||||
#### Scenario: Show validation progress
|
||||
|
||||
- **WHEN** validating multiple items (--all, --changes, or --specs)
|
||||
- **THEN** show progress indicator or status updates
|
||||
- **AND** indicate which item is currently being validated
|
||||
- **AND** display running count of passed/failed items
|
||||
|
||||
#### Scenario: Concurrency limits for performance
|
||||
|
||||
- **WHEN** validating multiple items
|
||||
- **THEN** run validations with a bounded concurrency (e.g., 4–8 in parallel)
|
||||
- **AND** ensure progress indicators remain responsive
|
||||
|
||||
### Requirement: Item type detection and ambiguity handling
|
||||
|
||||
The validate command SHALL handle ambiguous names and explicit type overrides to ensure clear, deterministic behavior.
|
||||
|
||||
#### Scenario: Direct item validation with automatic type detection
|
||||
|
||||
- **WHEN** executing `openspec validate <item-name>`
|
||||
- **THEN** if `<item-name>` uniquely matches a change or a spec, validate that item
|
||||
|
||||
#### Scenario: Ambiguity between change and spec names
|
||||
|
||||
- **GIVEN** `<item-name>` exists both as a change and as a spec
|
||||
- **WHEN** executing `openspec validate <item-name>`
|
||||
- **THEN** print an ambiguity error explaining both matches
|
||||
- **AND** suggest passing `--type change` or `--type spec`, or using `openspec change validate` / `openspec spec validate`
|
||||
- **AND** exit with code 1 without performing validation
|
||||
|
||||
#### Scenario: Unknown item name
|
||||
|
||||
- **WHEN** the `<item-name>` matches neither a change nor a spec
|
||||
- **THEN** print a not-found error
|
||||
- **AND** show nearest-match suggestions when available
|
||||
- **AND** exit with code 1
|
||||
|
||||
#### Scenario: Explicit type override
|
||||
|
||||
- **WHEN** executing `openspec validate --type change <item>`
|
||||
- **THEN** treat `<item>` as a change ID and validate it (skipping auto-detection)
|
||||
|
||||
- **WHEN** executing `openspec validate --type spec <item>`
|
||||
- **THEN** treat `<item>` as a spec ID and validate it (skipping auto-detection)
|
||||
|
||||
### Requirement: Interactivity controls
|
||||
|
||||
- The CLI SHALL respect `--no-interactive` to disable prompts.
|
||||
- The CLI SHALL respect `OPEN_SPEC_INTERACTIVE=0` to disable prompts globally.
|
||||
- Interactive prompts SHALL only be shown when stdin is a TTY and interactivity is not disabled.
|
||||
|
||||
#### Scenario: Disabling prompts via flags or environment
|
||||
|
||||
- **WHEN** `openspec validate` is executed with `--no-interactive` or with environment `OPEN_SPEC_INTERACTIVE=0`
|
||||
- **THEN** the CLI SHALL not display interactive prompts
|
||||
- **AND** SHALL print non-interactive hints or chosen outputs as appropriate
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
## Purpose
|
||||
|
||||
OpenSpec conventions SHALL define how system capabilities are documented, how changes are proposed and tracked, and how specifications evolve over time. This meta-specification serves as the source of truth for OpenSpec's own conventions.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Structured conventions for specs and changes
|
||||
|
||||
OpenSpec conventions SHALL mandate a structured spec format with clear requirement and scenario sections so tooling can parse consistently.
|
||||
@@ -135,6 +133,15 @@ The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Complete requirements using the structured format
|
||||
- Clear indication of change type for each requirement
|
||||
|
||||
#### Scenario: Using standard output symbols
|
||||
|
||||
- **WHEN** displaying delta operations in CLI output
|
||||
- **THEN** use these standard symbols:
|
||||
- `+` for ADDED (green)
|
||||
- `~` for MODIFIED (yellow)
|
||||
- `-` for REMOVED (red)
|
||||
- `→` for RENAMED (cyan)
|
||||
|
||||
### Requirement: Archive Process Enhancement
|
||||
|
||||
The archive process SHALL programmatically apply delta changes to current specifications using header-based matching.
|
||||
@@ -189,6 +196,34 @@ The system SHALL support multiple methods for reviewing proposed changes.
|
||||
- Command line: `diff -u specs/[capability]/spec.md changes/[name]/specs/[capability]/spec.md`
|
||||
- Any visual diff tool comparing current vs future state
|
||||
|
||||
### Requirement: Structured Format Adoption
|
||||
|
||||
Behavioral specifications SHALL adopt the structured format with `### Requirement:` and `#### Scenario:` headers as the default.
|
||||
|
||||
#### Scenario: Use structured headings for behavior
|
||||
|
||||
- **WHEN** documenting behavioral requirements
|
||||
- **THEN** use `### Requirement:` for requirements
|
||||
- **AND** use `#### Scenario:` for scenarios with bold WHEN/THEN/AND keywords
|
||||
|
||||
### Requirement: Verb–Noun CLI Command Structure
|
||||
OpenSpec CLI design SHALL use verbs as top-level commands with nouns provided as arguments or flags for scoping.
|
||||
|
||||
#### Scenario: Verb-first command discovery
|
||||
- **WHEN** a user runs a command like `openspec list`
|
||||
- **THEN** the verb communicates the action clearly
|
||||
- **AND** nouns refine scope via flags or arguments (e.g., `--changes`, `--specs`)
|
||||
|
||||
#### Scenario: Backward compatibility for noun commands
|
||||
- **WHEN** users run noun-prefixed commands such as `openspec spec ...` or `openspec change ...`
|
||||
- **THEN** the CLI SHALL continue to support them for at least one release
|
||||
- **AND** display a deprecation warning that points to verb-first alternatives
|
||||
|
||||
#### Scenario: Disambiguation guidance
|
||||
- **WHEN** item names are ambiguous between changes and specs
|
||||
- **THEN** `openspec show` and `openspec validate` SHALL accept `--type spec|change`
|
||||
- **AND** the help text SHALL document this clearly
|
||||
|
||||
## Core Principles
|
||||
|
||||
The system SHALL follow these principles:
|
||||
@@ -258,7 +293,6 @@ Behavioral specifications SHALL use a structured format with consistent section
|
||||
- Sub-bullets provide examples or specifics
|
||||
- Keep sub-bullets concise
|
||||
|
||||
|
||||
## Change Storage Convention
|
||||
|
||||
### Requirement: Header-Based Requirement Identification
|
||||
@@ -317,7 +351,6 @@ Change proposals SHALL store only the additions, modifications, and removals to
|
||||
- **AND** include reason for removal
|
||||
- **AND** document any migration path if applicable
|
||||
|
||||
|
||||
The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Delta files showing only what changes
|
||||
- Sections for ADDED, MODIFIED, REMOVED, and RENAMED requirements
|
||||
|
||||
@@ -162,6 +162,9 @@ changeCmd
|
||||
try {
|
||||
const changeCommand = new ChangeCommand();
|
||||
await changeCommand.validate(changeName, options);
|
||||
if (typeof process.exitCode === 'number' && process.exitCode !== 0) {
|
||||
process.exit(process.exitCode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${(error as Error).message}`);
|
||||
process.exitCode = 1;
|
||||
|
||||
+19
-5
@@ -206,16 +206,16 @@ export class ChangeCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
|
||||
const changeDir = path.join(changesPath, changeName);
|
||||
|
||||
try {
|
||||
await fs.access(proposalPath);
|
||||
await fs.access(changeDir);
|
||||
} catch {
|
||||
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
|
||||
throw new Error(`Change "${changeName}" not found at ${changeDir}`);
|
||||
}
|
||||
|
||||
const validator = new Validator(options?.strict || false);
|
||||
const report = await validator.validateChange(proposalPath);
|
||||
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
||||
|
||||
if (options?.json) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
@@ -223,12 +223,17 @@ export class ChangeCommand {
|
||||
if (report.valid) {
|
||||
console.log(`Change "${changeName}" is valid`);
|
||||
} else {
|
||||
console.error(`Change "${changeName}" has validation issues`);
|
||||
console.error(`Change "${changeName}" has issues`);
|
||||
report.issues.forEach(issue => {
|
||||
const label = issue.level === 'ERROR' ? 'ERROR' : 'WARNING';
|
||||
const prefix = issue.level === 'ERROR' ? '✗' : '⚠';
|
||||
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
||||
});
|
||||
// Next steps footer to guide fixing issues
|
||||
this.printNextSteps();
|
||||
if (!options?.json) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,4 +271,13 @@ export class ChangeCommand {
|
||||
|
||||
return { total, completed };
|
||||
}
|
||||
|
||||
private printNextSteps(): void {
|
||||
const bullets: string[] = [];
|
||||
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
||||
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
||||
bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
|
||||
console.error('Next steps:');
|
||||
bullets.forEach(b => console.error(` ${b}`));
|
||||
}
|
||||
}
|
||||
@@ -129,11 +129,12 @@ export class ValidateCommand {
|
||||
private async validateByType(type: ItemType, id: string, opts: { strict: boolean; json: boolean }): Promise<void> {
|
||||
const validator = new Validator(opts.strict);
|
||||
if (type === 'change') {
|
||||
const file = path.join(process.cwd(), 'openspec', 'changes', id, 'proposal.md');
|
||||
const changeDir = path.join(process.cwd(), 'openspec', 'changes', id);
|
||||
const start = Date.now();
|
||||
const report = await validator.validateChange(file);
|
||||
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
||||
const durationMs = Date.now() - start;
|
||||
this.printReport('change', id, report, durationMs, opts.json);
|
||||
// Non-zero exit if invalid (keeps enriched output test semantics)
|
||||
process.exitCode = report.valid ? 0 : 1;
|
||||
return;
|
||||
}
|
||||
@@ -195,8 +196,8 @@ export class ValidateCommand {
|
||||
for (const id of changeIds) {
|
||||
queue.push(async () => {
|
||||
const start = Date.now();
|
||||
const file = path.join(process.cwd(), 'openspec', 'changes', id, 'proposal.md');
|
||||
const report = await validator.validateChange(file);
|
||||
const changeDir = path.join(process.cwd(), 'openspec', 'changes', id);
|
||||
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
||||
const durationMs = Date.now() - start;
|
||||
return { id, type: 'change' as const, valid: report.valid, issues: report.issues, durationMs };
|
||||
});
|
||||
|
||||
+51
-41
@@ -59,16 +59,48 @@ export class ArchiveCommand {
|
||||
const validator = new Validator();
|
||||
let hasValidationErrors = false;
|
||||
|
||||
// Validate change.md file
|
||||
const changeFile = path.join(changeDir, 'change.md');
|
||||
// Validate proposal.md (non-blocking unless strict mode desired in future)
|
||||
const changeFile = path.join(changeDir, 'proposal.md');
|
||||
try {
|
||||
await fs.access(changeFile);
|
||||
const changeReport = await validator.validateChange(changeFile);
|
||||
|
||||
// Proposal validation is informative only (do not block archive)
|
||||
if (!changeReport.valid) {
|
||||
hasValidationErrors = true;
|
||||
console.log(chalk.red(`\nValidation errors in change.md:`));
|
||||
console.log(chalk.yellow(`\nProposal warnings in proposal.md (non-blocking):`));
|
||||
for (const issue of changeReport.issues) {
|
||||
const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
|
||||
console.log(chalk.yellow(` ${symbol} ${issue.message}`));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Change file doesn't exist, skip validation
|
||||
}
|
||||
|
||||
// Validate delta-formatted spec files under the change directory if present
|
||||
const changeSpecsDir = path.join(changeDir, 'specs');
|
||||
let hasDeltaSpecs = false;
|
||||
try {
|
||||
const candidates = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
||||
for (const c of candidates) {
|
||||
if (c.isDirectory()) {
|
||||
try {
|
||||
const candidatePath = path.join(changeSpecsDir, c.name, 'spec.md');
|
||||
await fs.access(candidatePath);
|
||||
const content = await fs.readFile(candidatePath, 'utf-8');
|
||||
if (/^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements/m.test(content)) {
|
||||
hasDeltaSpecs = true;
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
if (hasDeltaSpecs) {
|
||||
const deltaReport = await validator.validateChangeDeltaSpecs(changeDir);
|
||||
if (!deltaReport.valid) {
|
||||
hasValidationErrors = true;
|
||||
console.log(chalk.red(`\nValidation errors in change delta specs:`));
|
||||
for (const issue of deltaReport.issues) {
|
||||
if (issue.level === 'ERROR') {
|
||||
console.log(chalk.red(` ✗ ${issue.message}`));
|
||||
} else if (issue.level === 'WARNING') {
|
||||
@@ -76,41 +108,6 @@ export class ArchiveCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Change file doesn't exist, skip validation
|
||||
}
|
||||
|
||||
// Validate spec files
|
||||
const changeSpecsDir = path.join(changeDir, 'specs');
|
||||
try {
|
||||
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
|
||||
|
||||
try {
|
||||
await fs.access(specFile);
|
||||
const report = await validator.validateSpec(specFile);
|
||||
|
||||
if (!report.valid) {
|
||||
hasValidationErrors = true;
|
||||
console.log(chalk.red(`\nValidation errors in ${entry.name}/spec.md:`));
|
||||
for (const issue of report.issues) {
|
||||
if (issue.level === 'ERROR') {
|
||||
console.log(chalk.red(` ✗ ${issue.message}`));
|
||||
} else if (issue.level === 'WARNING') {
|
||||
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Spec file doesn't exist, skip validation
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No specs directory, skip validation
|
||||
}
|
||||
|
||||
if (hasValidationErrors) {
|
||||
@@ -200,9 +197,22 @@ export class ArchiveCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// All validations passed; write files and display counts
|
||||
// All validations passed; pre-validate rebuilt full spec and then write files and display counts
|
||||
let totals = { added: 0, modified: 0, removed: 0, renamed: 0 };
|
||||
for (const p of prepared) {
|
||||
const specName = path.basename(path.dirname(p.update.target));
|
||||
if (!options.noValidate) {
|
||||
const report = await new Validator().validateSpecContent(specName, p.rebuilt);
|
||||
if (!report.valid) {
|
||||
console.log(chalk.red(`\nValidation errors in rebuilt spec for ${specName} (will not write changes):`));
|
||||
for (const issue of report.issues) {
|
||||
if (issue.level === 'ERROR') console.log(chalk.red(` ✗ ${issue.message}`));
|
||||
else if (issue.level === 'WARNING') console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
||||
}
|
||||
console.log('Aborted. No files were changed.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.writeUpdatedSpec(p.update, p.rebuilt, p.counts);
|
||||
totals.added += p.counts.added;
|
||||
totals.modified += p.counts.modified;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z, ZodError } from 'zod';
|
||||
import { readFileSync } from 'fs';
|
||||
import { readFileSync, promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { SpecSchema, ChangeSchema, Spec, Change } from '../schemas/index.js';
|
||||
import { MarkdownParser } from '../parsers/markdown-parser.js';
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
MAX_REQUIREMENT_TEXT_LENGTH,
|
||||
VALIDATION_MESSAGES
|
||||
} from './constants.js';
|
||||
import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
|
||||
|
||||
export class Validator {
|
||||
private strictMode: boolean;
|
||||
@@ -48,6 +49,27 @@ export class Validator {
|
||||
return this.createReport(issues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate spec content from a string (used for pre-write validation of rebuilt specs)
|
||||
*/
|
||||
async validateSpecContent(specName: string, content: string): Promise<ValidationReport> {
|
||||
const issues: ValidationIssue[] = [];
|
||||
try {
|
||||
const parser = new MarkdownParser(content);
|
||||
const spec = parser.parseSpec(specName);
|
||||
const result = SpecSchema.safeParse(spec);
|
||||
if (!result.success) {
|
||||
issues.push(...this.convertZodErrors(result.error));
|
||||
}
|
||||
issues.push(...this.applySpecRules(spec, content));
|
||||
} catch (error) {
|
||||
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
const enriched = this.enrichTopLevelError(specName, baseMessage);
|
||||
issues.push({ level: 'ERROR', path: 'file', message: enriched });
|
||||
}
|
||||
return this.createReport(issues);
|
||||
}
|
||||
|
||||
async validateChange(filePath: string): Promise<ValidationReport> {
|
||||
const issues: ValidationIssue[] = [];
|
||||
const changeName = this.extractNameFromPath(filePath);
|
||||
@@ -79,6 +101,148 @@ export class Validator {
|
||||
return this.createReport(issues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate delta-formatted spec files under a change directory.
|
||||
* Enforces:
|
||||
* - At least one delta across all files
|
||||
* - ADDED/MODIFIED: each requirement has SHALL/MUST and at least one scenario
|
||||
* - REMOVED: names only; no scenario/description required
|
||||
* - RENAMED: pairs well-formed
|
||||
* - No duplicates within sections; no cross-section conflicts per spec
|
||||
*/
|
||||
async validateChangeDeltaSpecs(changeDir: string): Promise<ValidationReport> {
|
||||
const issues: ValidationIssue[] = [];
|
||||
const specsDir = path.join(changeDir, 'specs');
|
||||
let totalDeltas = 0;
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const specName = entry.name;
|
||||
const specFile = path.join(specsDir, specName, 'spec.md');
|
||||
let content: string | undefined;
|
||||
try {
|
||||
content = await fs.readFile(specFile, 'utf-8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const plan = parseDeltaSpec(content);
|
||||
const entryPath = `${specName}/spec.md`;
|
||||
|
||||
const addedNames = new Set<string>();
|
||||
const modifiedNames = new Set<string>();
|
||||
const removedNames = new Set<string>();
|
||||
const renamedFrom = new Set<string>();
|
||||
const renamedTo = new Set<string>();
|
||||
|
||||
// Validate ADDED
|
||||
for (const block of plan.added) {
|
||||
const key = normalizeRequirementName(block.name);
|
||||
totalDeltas++;
|
||||
if (addedNames.has(key)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in ADDED: "${block.name}"` });
|
||||
} else {
|
||||
addedNames.add(key);
|
||||
}
|
||||
const requirementText = this.extractRequirementText(block.raw);
|
||||
if (!requirementText) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" is missing requirement text` });
|
||||
} else if (!this.containsShallOrMust(requirementText)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must contain SHALL or MUST` });
|
||||
}
|
||||
const scenarioCount = this.countScenarios(block.raw);
|
||||
if (scenarioCount < 1) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario` });
|
||||
}
|
||||
}
|
||||
|
||||
// Validate MODIFIED
|
||||
for (const block of plan.modified) {
|
||||
const key = normalizeRequirementName(block.name);
|
||||
totalDeltas++;
|
||||
if (modifiedNames.has(key)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in MODIFIED: "${block.name}"` });
|
||||
} else {
|
||||
modifiedNames.add(key);
|
||||
}
|
||||
const requirementText = this.extractRequirementText(block.raw);
|
||||
if (!requirementText) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" is missing requirement text` });
|
||||
} else if (!this.containsShallOrMust(requirementText)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must contain SHALL or MUST` });
|
||||
}
|
||||
const scenarioCount = this.countScenarios(block.raw);
|
||||
if (scenarioCount < 1) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must include at least one scenario` });
|
||||
}
|
||||
}
|
||||
|
||||
// Validate REMOVED (names only)
|
||||
for (const name of plan.removed) {
|
||||
const key = normalizeRequirementName(name);
|
||||
totalDeltas++;
|
||||
if (removedNames.has(key)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in REMOVED: "${name}"` });
|
||||
} else {
|
||||
removedNames.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate RENAMED pairs
|
||||
for (const { from, to } of plan.renamed) {
|
||||
const fromKey = normalizeRequirementName(from);
|
||||
const toKey = normalizeRequirementName(to);
|
||||
totalDeltas++;
|
||||
if (renamedFrom.has(fromKey)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate FROM in RENAMED: "${from}"` });
|
||||
} else {
|
||||
renamedFrom.add(fromKey);
|
||||
}
|
||||
if (renamedTo.has(toKey)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate TO in RENAMED: "${to}"` });
|
||||
} else {
|
||||
renamedTo.add(toKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-section conflicts (within the same spec file)
|
||||
for (const n of modifiedNames) {
|
||||
if (removedNames.has(n)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and REMOVED: "${n}"` });
|
||||
}
|
||||
if (addedNames.has(n)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and ADDED: "${n}"` });
|
||||
}
|
||||
}
|
||||
for (const n of addedNames) {
|
||||
if (removedNames.has(n)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both ADDED and REMOVED: "${n}"` });
|
||||
}
|
||||
}
|
||||
for (const { from, to } of plan.renamed) {
|
||||
const fromKey = normalizeRequirementName(from);
|
||||
const toKey = normalizeRequirementName(to);
|
||||
if (modifiedNames.has(fromKey)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED references old name from RENAMED. Use new header for "${to}"` });
|
||||
}
|
||||
if (addedNames.has(toKey)) {
|
||||
issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO collides with ADDED for "${to}"` });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If no specs dir, treat as no deltas
|
||||
}
|
||||
|
||||
if (totalDeltas === 0) {
|
||||
issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
|
||||
}
|
||||
|
||||
return this.createReport(issues);
|
||||
}
|
||||
|
||||
private convertZodErrors(error: ZodError): ValidationIssue[] {
|
||||
return error.issues.map(err => {
|
||||
let message = err.message;
|
||||
@@ -206,4 +370,27 @@ export class Validator {
|
||||
isValid(report: ValidationReport): boolean {
|
||||
return report.valid;
|
||||
}
|
||||
|
||||
private extractRequirementText(blockRaw: string): string | undefined {
|
||||
const lines = blockRaw.split('\n');
|
||||
// Skip header
|
||||
let i = 1;
|
||||
const bodyLines: string[] = [];
|
||||
for (; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (/^####\s+/.test(line)) break; // scenarios start
|
||||
bodyLines.push(line);
|
||||
}
|
||||
const text = bodyLines.join('\n').split('\n').map(l => l.trim()).find(l => l.length > 0);
|
||||
return text;
|
||||
}
|
||||
|
||||
private containsShallOrMust(text: string): boolean {
|
||||
return /\b(SHALL|MUST)\b/.test(text);
|
||||
}
|
||||
|
||||
private countScenarios(blockRaw: string): number {
|
||||
const matches = blockRaw.match(/^####\s+/gm);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user