Compare commits

...
61 changed files with 972 additions and 163 deletions
@@ -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,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,4 +1,4 @@
## MODIFIED Requirements
## ADDED Requirements
### Requirement: Diff Command Enhancement
@@ -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.
@@ -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.
@@ -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
@@ -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)
+75 -18
View File
@@ -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
+91
View File
@@ -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`
+32 -29
View File
@@ -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
+11 -37
View File
@@ -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
+85
View File
@@ -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
+87
View File
@@ -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
+22 -2
View File
@@ -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
+201
View File
@@ -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
+37 -4
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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}`));
}
}
+5 -4
View File
@@ -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
View File
@@ -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;
+188 -1
View File
@@ -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;
}
}