mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a3bfae784 | ||
|
|
fae08072a9 | ||
|
|
07df6c97c9 | ||
|
|
7b13a2de03 | ||
|
|
41fc14d360 | ||
|
|
7c0face31b | ||
|
|
332816cc35 | ||
|
|
7ced2a8791 | ||
|
|
a79b8b5c03 | ||
|
|
52d620e40e | ||
|
|
22082338fd | ||
|
|
6458b6ed39 | ||
|
|
01a2f5d600 | ||
|
|
95d855d641 | ||
|
|
562530dfa8 |
@@ -11,10 +11,11 @@ if (existsSync('dist')) {
|
||||
rmSync('dist', { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Run TypeScript compiler
|
||||
// Run TypeScript compiler (use local version explicitly)
|
||||
console.log('Compiling TypeScript...');
|
||||
try {
|
||||
execSync('tsc', { stdio: 'inherit' });
|
||||
execSync('./node_modules/.bin/tsc -v', { stdio: 'inherit' });
|
||||
execSync('./node_modules/.bin/tsc', { stdio: 'inherit' });
|
||||
console.log('\n✅ Build completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('\n❌ Build failed!');
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+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
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Design: Verb–Noun CLI Structure Adoption
|
||||
|
||||
## Overview
|
||||
We will make verb commands (`list`, `show`, `validate`, `diff`, `archive`) the primary interface and keep noun commands (`spec`, `change`) as deprecated aliases for one release.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. Keep routing centralized in `src/cli/index.ts`.
|
||||
2. Add `--specs`/`--changes` to `openspec list`, with `--changes` as default.
|
||||
3. Show deprecation warnings for `openspec change list` and, more generally, for any `openspec change ...` and `openspec spec ...` subcommands.
|
||||
4. Do not change `show`/`validate` behavior beyond help text; they already support `--type` for disambiguation.
|
||||
|
||||
## Backward Compatibility
|
||||
All noun-based commands continue to work with clear deprecation warnings directing users to verb-first equivalents.
|
||||
|
||||
## Out of Scope
|
||||
JSON output parity for `openspec list` across modes and `show --specs/--changes` discovery are follow-ups.
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Change: Adopt Verb–Noun CLI Structure (Deprecate Noun-Based Commands)
|
||||
|
||||
## Why
|
||||
|
||||
Most widely used CLIs (git, docker, kubectl) start with an action (verb) followed by the object (noun). This matches how users think: “do X to Y”. Using verbs as top-level commands improves clarity, discoverability, and extensibility.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Promote top-level verb commands as primary entry points: `list`, `show`, `validate`, `diff`, `archive`.
|
||||
- Deprecate noun-based top-level commands: `openspec spec ...` and `openspec change ...`.
|
||||
- Introduce consistent noun scoping via flags where applicable (e.g., `--changes`, `--specs`) and keep smart defaults.
|
||||
- Clarify disambiguation for `show` and `validate` when names collide.
|
||||
|
||||
### Mappings (From → To)
|
||||
|
||||
- **List**
|
||||
- From: `openspec change list`
|
||||
- To: `openspec list --changes` (default), or `openspec list --specs`
|
||||
|
||||
- **Show**
|
||||
- From: `openspec spec show <spec-id>` / `openspec change show <change-id>`
|
||||
- To: `openspec show <item-id>` with auto-detect, use `--type spec|change` if ambiguous
|
||||
|
||||
- **Validate**
|
||||
- From: `openspec spec validate <spec-id>` / `openspec change validate <change-id>`
|
||||
- To: `openspec validate <item-id> --type spec|change`, or bulk: `openspec validate --specs` / `--changes` / `--all`
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
- Keep `openspec spec` and `openspec change` available with deprecation warnings for one release cycle.
|
||||
- Update help text to point users to the verb–noun alternatives.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected specs**:
|
||||
- `cli-list`: Add support for `--specs` and explicit `--changes` (default remains changes)
|
||||
- `openspec-conventions`: Add explicit requirement establishing verb–noun CLI design and deprecation guidance
|
||||
- **Affected code**:
|
||||
- `src/cli/index.ts`: Un-deprecate top-level `list`; mark `change list` as deprecated; ensure help text and warnings align
|
||||
- `src/core/list.ts`: Support listing specs via `--specs` and default to changes; shared output shape
|
||||
- Optional follow-ups: tighten `show`/`validate` help and ambiguity handling
|
||||
|
||||
## Explicit Changes
|
||||
|
||||
**CLI Design**
|
||||
- From: Mixed model with nouns (`spec`, `change`) and some top-level verbs; `openspec list` currently deprecated
|
||||
- To: Verbs as primary: `openspec list|show|validate|diff|archive`; nouns scoped via flags or item ids; noun commands deprecated
|
||||
- Reason: Align with common CLIs; improve UX; simpler mental model
|
||||
- Impact: Non-breaking with deprecation period; users migrate incrementally
|
||||
|
||||
**Listing Behavior**
|
||||
- From: `openspec change list` (primary), `openspec list` (deprecated)
|
||||
- To: `openspec list` as primary, defaulting to `--changes`; add `--specs` to list specs
|
||||
- Reason: Consistent verb–noun style; better discoverability
|
||||
- Impact: New option; preserves existing behavior via default
|
||||
|
||||
## Rollout and Deprecation Policy
|
||||
|
||||
- Show deprecation warnings on noun-based commands for one release.
|
||||
- Document new usage in `openspec/README.md` and CLI help.
|
||||
- After one release, consider removing noun-based commands, or keep as thin aliases without warnings.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `show` also accept `--changes`/`--specs` for discovery without an id? (Out of scope here; current auto-detect and `--type` remain.)
|
||||
|
||||
|
||||
+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)
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Delta: OpenSpec Conventions — Verb–Noun CLI Design
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. CLI Behavior and Help
|
||||
- [x] 1.1 Un-deprecate top-level `openspec list`; mark `change list` as deprecated with warning that points to `openspec list`
|
||||
- [x] 1.2 Add support to list specs via `openspec list --specs` and keep `--changes` as default
|
||||
- [x] 1.3 Update command descriptions and `--help` output to emphasize verb–noun pattern
|
||||
- [x] 1.4 Keep `openspec spec ...` and `openspec change ...` commands working but print deprecation notices
|
||||
|
||||
## 2. Core List Logic
|
||||
- [x] 2.1 Extend `src/core/list.ts` to accept a mode: `changes` (default) or `specs`
|
||||
- [x] 2.2 Implement `specs` listing: scan `openspec/specs/*/spec.md`, compute requirement count via parser, format output consistently
|
||||
- [x] 2.3 Share output structure for both modes; preserve current text table; ensure JSON parity in future change
|
||||
|
||||
## 3. Specs and Conventions
|
||||
- [x] 3.1 Update `openspec/specs/cli-list/spec.md` to document `--specs` (and default to changes)
|
||||
- [x] 3.2 Update `openspec/specs/openspec-conventions/spec.md` with a requirement for verb–noun CLI design and deprecation guidance
|
||||
|
||||
## 4. Tests and Docs
|
||||
- [x] 4.1 Update tests: ensure `openspec list` works for changes and specs; keep `change list` tests but assert warning
|
||||
- [ ] 4.2 Update README and any usage docs to show new primary commands
|
||||
- [ ] 4.3 Add migration notes in repo CHANGELOG or README
|
||||
|
||||
## 5. Follow-ups (Optional, not in this change)
|
||||
- [ ] 5.1 Consider `openspec show --specs/--changes` for discovery without ids
|
||||
- [ ] 5.2 Consider JSON output for `openspec list` with `--json` for both modes
|
||||
|
||||
|
||||
+9
-1
@@ -106,6 +106,8 @@ Where `Issue` follows the existing per-item validation report shape `{ level: "E
|
||||
|
||||
### 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>`
|
||||
@@ -138,4 +140,10 @@ Where `Issue` follows the existing per-item validation report shape `{ level: "E
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
@@ -0,0 +1,25 @@
|
||||
# improve-validate-error-messages
|
||||
|
||||
## Why
|
||||
|
||||
Developers struggle to resolve validation failures because current errors lack actionable guidance. Common issues include: missing deltas, missing required sections, and misformatted scenarios that are silently ignored. Without clear remediation steps, users cannot quickly correct structure or formatting, leading to frustration and rework. Improving error messages with concrete fixes, file/section hints, and suggested commands will significantly reduce time-to-green and make OpenSpec more approachable.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Validation errors SHALL include specific remediation steps (what to change and where).
|
||||
- "No deltas found" error SHALL guide users to create `specs/` with proper delta headers and suggest debug commands.
|
||||
- Missing required sections (Spec: Purpose/Requirements; Change: Why/What Changes) SHALL include expected header names and a minimal skeleton example.
|
||||
- Likely misformatted scenarios (bulleted WHEN/THEN/AND) SHALL emit a targeted warning explaining the `#### Scenario:` format and show a conversion template.
|
||||
- All reported issues SHALL include the source file path and structured location (e.g., `deltas[0].requirements[0]`).
|
||||
- Non-JSON output SHOULD end with a short "Next steps" footer when invalid.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected CLI: validate
|
||||
- Affected code:
|
||||
- `src/commands/validate.ts`
|
||||
- `src/core/validation/validator.ts`
|
||||
- `src/core/validation/constants.ts`
|
||||
- `src/core/parsers/*` (wrapping thrown errors with richer context)
|
||||
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Validate Command
|
||||
|
||||
## ADDED 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`
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
## 1. Enhance validation messages
|
||||
- [x] 1.1 Add remediation guidance for "No deltas found"
|
||||
- [x] 1.2 Include file path and structured path in all issues
|
||||
- [x] 1.3 Improve messages for missing required sections (Spec, Change)
|
||||
- [x] 1.4 Detect likely misformatted scenarios and warn with conversion example
|
||||
- [x] 1.5 Add "Next steps" footer for non-JSON invalid output
|
||||
|
||||
## 2. Update constants and helpers
|
||||
- [x] 2.1 Centralize guidance snippets in `VALIDATION_MESSAGES`
|
||||
- [x] 2.2 Provide minimal skeleton examples for missing sections
|
||||
|
||||
## 3. Parser integration
|
||||
- [x] 3.1 Capture parser-thrown errors and wrap with richer context
|
||||
- [x] 3.2 Add file/section references to surfaced parser errors
|
||||
|
||||
## 4. Tests
|
||||
- [x] 4.1 Unit tests for validator message composition
|
||||
- [x] 4.2 CLI integration tests for human-readable output (with footer)
|
||||
- [x] 4.3 JSON mode tests (structure unchanged, content enriched)
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# Design: Agent Instructions Update
|
||||
|
||||
## Approach
|
||||
|
||||
### Information Architecture
|
||||
- **Front-load critical information** - Three-stage workflow comes first
|
||||
- **Clear hierarchy** - Core Workflow → Quick Start → Commands → Details → Edge Cases
|
||||
- **50% length reduction** - Target ~285 lines from current ~575 lines
|
||||
- **Imperative mood** - "Create proposal" vs "You should create a proposal"
|
||||
- **Bullet points over paragraphs** - Scannable, concise information
|
||||
|
||||
### Three-Stage Workflow Documentation
|
||||
The workflow is now prominently featured as a core concept:
|
||||
1. **Creating** - Proposal generation phase
|
||||
2. **Implementing** - Code development phase with explicit steps:
|
||||
- Read proposal.md for understanding
|
||||
- Read design.md for technical context
|
||||
- Read tasks.md for checklist
|
||||
- Implement tasks sequentially
|
||||
- Mark complete immediately after each task
|
||||
3. **Archiving** - Post-deployment finalization phase
|
||||
|
||||
This structure helps agents understand the lifecycle and their role at each stage. The implementation phase is particularly detailed to prevent common mistakes like skipping documentation or batching task completion.
|
||||
|
||||
### CLI Documentation Updates
|
||||
- **Comprehensive command coverage** - All 9 primary commands documented
|
||||
- **`openspec list` prominence** - Essential for discovering changes and specs
|
||||
- **Interactive mode documentation** - How agents can use prompts effectively
|
||||
- **Complete flag documentation** - All options like --json, --type, --skip-specs
|
||||
- **Deprecation cleanup** - Remove noun-first patterns (openspec change show)
|
||||
|
||||
### Agent-Specific Enhancements
|
||||
Based on industry best practices for coding agents (Claude Code, Cursor, etc.):
|
||||
|
||||
**Implementation Workflow**
|
||||
- Explicit steps prevent skipping critical context
|
||||
- Reading proposal/design first ensures understanding before coding
|
||||
- Sequential task completion maintains focus
|
||||
- Immediate marking prevents losing track of progress
|
||||
- Addresses common failure mode: jumping straight to code
|
||||
|
||||
**Spec Discovery Workflow**
|
||||
- Always check existing specs before creating new ones
|
||||
- Use `openspec list --specs` to discover current capabilities
|
||||
- Prefer modifying existing specs over creating duplicates
|
||||
- Prevents fragmentation and maintains coherent architecture
|
||||
|
||||
**Decision Clarity**
|
||||
- Clear decision trees eliminating ambiguous conditions
|
||||
- Concrete examples for each decision branch
|
||||
- Simplified bug vs feature determination
|
||||
|
||||
**Tool Usage Guidance**
|
||||
- Tool selection matrix (when to use Grep vs Glob vs Read)
|
||||
- Error recovery patterns for common failures
|
||||
- Verification workflows to confirm correctness
|
||||
|
||||
**Context Management**
|
||||
- "Before Any Task" checklist for gathering context
|
||||
- What to read before starting any work
|
||||
- How to maintain state across interactions
|
||||
|
||||
**Spec File Structure Documentation**
|
||||
- Complete examples with ADDED/MODIFIED/REMOVED sections
|
||||
- Critical scenario formatting (#### Scenario: headers)
|
||||
- Delta file location clarity (changes/{name}/specs/)
|
||||
- Addresses most common creation errors from retrospective
|
||||
|
||||
**Troubleshooting and Debugging**
|
||||
- Common error messages with solutions
|
||||
- Delta detection debugging steps
|
||||
- Validation best practices
|
||||
- JSON output for inspection
|
||||
- Prevents hours of frustration from silent failures
|
||||
|
||||
**Best Practices**
|
||||
- Be concise (one-line answers when appropriate)
|
||||
- Be specific (file.ts:42 line references)
|
||||
- Start simple (<100 lines, single-file defaults)
|
||||
- Justify complexity (require metrics/data)
|
||||
|
||||
## Design Rationale
|
||||
|
||||
### Why These Changes Matter
|
||||
|
||||
**Cognitive Load Reduction**
|
||||
- Agents process instructions better with clear structure
|
||||
- Front-loading critical info reduces scanning time
|
||||
- Decision trees eliminate analysis paralysis
|
||||
|
||||
**Industry Alignment**
|
||||
- Follows patterns proven effective in Claude Code, Cursor, GitHub Copilot
|
||||
- Addresses common failure modes (ambiguous decisions, missing context)
|
||||
- Optimizes for LLM strengths (pattern matching) vs weaknesses (calculations)
|
||||
|
||||
**Addressing Critical Pain Points (from Retrospective)**
|
||||
- **Scenario formatting** - Biggest struggle, now explicitly documented with examples
|
||||
- **Complete spec structure** - Full examples prevent structural errors
|
||||
- **Delta detection issues** - Debugging commands help diagnose problems
|
||||
- **Silent parsing failures** - Troubleshooting section explains common issues
|
||||
|
||||
**Practical Impact**
|
||||
- Faster agent comprehension of tasks
|
||||
- Fewer misinterpretations of requirements
|
||||
- More consistent implementation quality
|
||||
- Better error recovery when things go wrong
|
||||
- Prevents the most common errors identified in user experience
|
||||
|
||||
## Trade-offs
|
||||
|
||||
### What We're Removing
|
||||
- Lengthy explanations of concepts that can be inferred
|
||||
- Redundant examples that don't add clarity
|
||||
- Verbose edge case documentation (moved to reference section)
|
||||
- Deprecated command documentation
|
||||
|
||||
### What We're Keeping
|
||||
- All critical workflow steps
|
||||
- Complete CLI command reference
|
||||
- Complexity management principles
|
||||
- Directory structure visualization
|
||||
- Quick reference summary
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The CLAUDE.md template is intentionally more concise than README.md since:
|
||||
- It appears in every project root
|
||||
- Agents can reference the full README.md for details
|
||||
- It needs to load quickly in AI context windows
|
||||
- Focus is on immediate actionable guidance
|
||||
@@ -0,0 +1,117 @@
|
||||
# Update OpenSpec Agent Instructions
|
||||
|
||||
## Why
|
||||
|
||||
The current OpenSpec agent instructions need updates to follow best practices for AI assistant instructions (brevity, clarity, removing ambiguity), ensure CLI commands are current with the actual implementation, and properly document the three-stage workflow pattern that agents should follow.
|
||||
|
||||
## What Changes
|
||||
|
||||
### Core Structure Improvements
|
||||
- **Front-load the 3-stage workflow** as the primary mental model:
|
||||
1. Creating a change proposal (proposal.md, spec deltas, design.md, tasks.md)
|
||||
2. Implementing a change proposal:
|
||||
- First read proposal.md to understand the change
|
||||
- Read design.md if it exists for technical context
|
||||
- Read tasks.md for the implementation checklist
|
||||
- Complete tasks one by one
|
||||
- Mark each task complete immediately after finishing
|
||||
3. Archiving the change proposal (using archive command after deployment)
|
||||
- **Reduce instruction length by 50%** while maintaining all critical information
|
||||
- **Restructure with clear hierarchy**: Core Workflow → Quick Start → Commands → Details → Edge Cases
|
||||
|
||||
### Decision Clarity Enhancements
|
||||
- **Add clear decision trees** for common scenarios (bug vs feature, proposal needed vs not)
|
||||
- **Remove ambiguous conditions** that confuse agent decision-making
|
||||
- **Add "Before Any Task" checklist** for context gathering
|
||||
- **Add "Before Creating Specs" rule** - Always check existing specs first to avoid duplicates
|
||||
|
||||
### CLI Documentation Updates
|
||||
- **Complete command documentation** with all current functionality:
|
||||
- `openspec init [path]` - Initialize OpenSpec in a project
|
||||
- `openspec list` - List all active changes (default)
|
||||
- `openspec list --specs` - List all specifications
|
||||
- `openspec show [item]` - Display change or spec with auto-detection
|
||||
- `openspec show` - Interactive mode for selection
|
||||
- `openspec diff [change]` - Show spec differences for a change
|
||||
- `openspec validate [item]` - Validate changes or specs
|
||||
- `openspec archive [change]` - Archive completed change after deployment
|
||||
- `openspec update [path]` - Update OpenSpec instruction files
|
||||
- **Document all flags and options**:
|
||||
- `--json` output format for programmatic use
|
||||
- `--type change|spec` for disambiguation
|
||||
- `--skip-specs` for tooling-only archives
|
||||
- `--strict` for strict validation mode
|
||||
- `--no-interactive` to disable prompts
|
||||
- **Remove deprecated command references** (noun-first patterns like `openspec change show`)
|
||||
- **Add concrete examples** for each command variation
|
||||
- **Document debugging commands**:
|
||||
- `openspec show [change] --json --deltas-only` for inspecting deltas
|
||||
- `openspec validate [change] --strict` for comprehensive validation
|
||||
|
||||
### Spec File Structure Documentation
|
||||
- **Complete spec file examples** showing proper structure:
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: Clear requirement statement
|
||||
The system SHALL provide the functionality...
|
||||
|
||||
#### Scenario: Descriptive scenario name
|
||||
- **WHEN** condition occurs
|
||||
- **THEN** expected outcome
|
||||
- **AND** additional outcomes
|
||||
```
|
||||
- **Scenario formatting requirements** (critical - most common error):
|
||||
- MUST use `#### Scenario:` headers (4 hashtags)
|
||||
- NOT bullet lists or bold text
|
||||
- Each requirement MUST have at least one scenario
|
||||
- **Delta file location** - Clear explanation:
|
||||
- Spec files go in `changes/{name}/specs/` directory
|
||||
- Deltas are automatically extracted from these files
|
||||
- Use operation prefixes: ADDED, MODIFIED, REMOVED, RENAMED
|
||||
|
||||
### Troubleshooting Section
|
||||
- **Common errors and solutions**:
|
||||
- "Change must have at least one delta" → Check specs/ directory exists with .md files
|
||||
- "Requirement must have at least one scenario" → Check scenario uses `#### Scenario:` format
|
||||
- Silent scenario parsing failures → Verify exact header format
|
||||
- **Delta detection debugging**:
|
||||
- Use `openspec show [change] --json --deltas-only` to inspect parsed deltas
|
||||
- Check that spec files have operation prefixes (## ADDED Requirements)
|
||||
- Verify specs/ subdirectory structure
|
||||
- **Validation best practices**:
|
||||
- Always use `--strict` flag for comprehensive checks
|
||||
- Use JSON output for debugging: `--json | jq '.deltas'`
|
||||
|
||||
### Agent-Specific Improvements
|
||||
- **Implementation workflow** - Clear step-by-step process:
|
||||
1. Read proposal.md to understand what's being built
|
||||
2. Read design.md (if exists) for technical decisions
|
||||
3. Read tasks.md for the implementation checklist
|
||||
4. Implement tasks one by one in order
|
||||
5. Mark each task complete immediately: `- [x] Task completed`
|
||||
6. Never skip ahead or batch task completion
|
||||
- **Spec discovery workflow** - Always check existing specs before creating new ones:
|
||||
- Use `openspec list --specs` to see all current specs
|
||||
- Check if capability already exists before creating
|
||||
- Prefer modifying existing specs over creating duplicates
|
||||
- **Tool selection matrix** - When to use Grep vs Glob vs Read
|
||||
- **Error recovery patterns** - How to handle common failures
|
||||
- **Context management guide** - What to read before starting tasks
|
||||
- **Verification workflows** - How to confirm changes are correct
|
||||
|
||||
### Best Practices Section
|
||||
- **Be concise** - One-line answers when appropriate
|
||||
- **Be specific** - Use exact file paths and line numbers (file.ts:42)
|
||||
- **Start simple** - Default to <100 lines, single-file implementations
|
||||
- **Justify complexity** - Require data/metrics for any optimization
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: None (this is a tooling/documentation change)
|
||||
- Affected code:
|
||||
- `src/core/templates/claude-template.ts` - Update CLAUDE.md template
|
||||
- Affected documentation:
|
||||
- `openspec/README.md` - Main OpenSpec instructions
|
||||
- CLAUDE.md files generated by `openspec init` command
|
||||
|
||||
Note: This is a tooling/infrastructure change that doesn't require spec updates. When archiving, use `openspec archive update-agent-instructions --skip-specs`.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Restructure OpenSpec README.md
|
||||
- [ ] 1.1 Front-load the three-stage workflow as primary content
|
||||
- [ ] 1.2 Restructure with hierarchy: Core Workflow → Quick Start → Commands → Details → Edge Cases
|
||||
- [ ] 1.3 Reduce total length by 50% (target: ~285 lines from current ~575)
|
||||
- [ ] 1.4 Add "Before Any Task" context-gathering checklist
|
||||
- [ ] 1.5 Add "Before Creating Specs" rule to check existing specs first
|
||||
|
||||
## 2. Add Decision Clarity
|
||||
- [ ] 2.1 Create clear decision trees for "Create Proposal?" scenarios
|
||||
- [ ] 2.2 Remove ambiguous conditions that confuse agents
|
||||
- [ ] 2.3 Add concrete examples for each decision branch
|
||||
- [ ] 2.4 Simplify bug vs feature determination logic
|
||||
- [ ] 2.5 Add explicit Stage 2 implementation steps (read → implement → mark complete)
|
||||
|
||||
## 3. Update CLI Documentation
|
||||
- [ ] 3.1 Document `openspec list` and `openspec list --specs` commands
|
||||
- [ ] 3.2 Document `openspec show` with all flags and interactive mode
|
||||
- [ ] 3.3 Document `openspec diff [change]` for viewing spec differences
|
||||
- [ ] 3.4 Document `openspec archive` with --skip-specs option
|
||||
- [ ] 3.5 Document `openspec validate` with --strict and batch modes
|
||||
- [ ] 3.6 Document `openspec init` and `openspec update` commands
|
||||
- [ ] 3.7 Remove all deprecated noun-first command references
|
||||
- [ ] 3.8 Add concrete usage examples for each command variation
|
||||
- [ ] 3.9 Document all flags: --json, --type, --no-interactive, etc.
|
||||
- [ ] 3.10 Document debugging commands: `show --json --deltas-only`
|
||||
|
||||
## 4. Add Spec File Documentation
|
||||
- [ ] 4.1 Add complete spec file structure example with ADDED/MODIFIED sections
|
||||
- [ ] 4.2 Document scenario formatting requirements (#### Scenario: headers)
|
||||
- [ ] 4.3 Explain delta file location (changes/{name}/specs/ directory)
|
||||
- [ ] 4.4 Show how deltas are automatically extracted
|
||||
- [ ] 4.5 Include warning about most common error (scenario formatting)
|
||||
|
||||
## 5. Add Troubleshooting Section
|
||||
- [ ] 5.1 Document common errors and their solutions
|
||||
- [ ] 5.2 Add delta detection debugging steps
|
||||
- [ ] 5.3 Include validation best practices (--strict flag)
|
||||
- [ ] 5.4 Show how to use JSON output for debugging
|
||||
- [ ] 5.5 Add examples of silent parsing failures
|
||||
|
||||
## 6. Add Agent-Specific Sections
|
||||
- [ ] 6.1 Add implementation workflow (read docs → implement tasks → mark complete)
|
||||
- [ ] 6.2 Add spec discovery workflow (check existing before creating)
|
||||
- [ ] 6.3 Create tool selection matrix (Grep vs Glob vs Read)
|
||||
- [ ] 6.4 Add error recovery patterns section
|
||||
- [ ] 6.5 Add context management guide
|
||||
- [ ] 6.6 Add verification workflows section
|
||||
- [ ] 6.7 Add best practices section (concise, specific, simple)
|
||||
|
||||
## 7. Update CLAUDE.md Template
|
||||
- [ ] 7.1 Update `src/core/templates/claude-template.ts` with streamlined content
|
||||
- [ ] 7.2 Include three-stage workflow prominently
|
||||
- [ ] 7.3 Add comprehensive CLI quick reference (list, show, diff, archive, etc.)
|
||||
- [ ] 7.4 Add "Before Any Task" checklist
|
||||
- [ ] 7.5 Add "Before Creating Specs" rule
|
||||
- [ ] 7.6 Keep complexity management principles
|
||||
- [ ] 7.7 Add critical scenario formatting note (#### Scenario: headers)
|
||||
- [ ] 7.8 Include debugging command reference
|
||||
|
||||
## 8. Testing and Validation
|
||||
- [ ] 8.1 Test all documented CLI commands for accuracy
|
||||
- [ ] 8.2 Run `openspec init` to verify CLAUDE.md generation
|
||||
- [ ] 8.3 Validate instruction clarity with example scenarios
|
||||
- [ ] 8.4 Ensure no critical information was lost in streamlining
|
||||
- [ ] 8.5 Verify decision trees eliminate ambiguity
|
||||
- [ ] 8.6 Test scenario formatting examples work correctly
|
||||
- [ ] 8.7 Verify troubleshooting steps resolve common errors
|
||||
@@ -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
|
||||
|
||||
@@ -129,8 +126,6 @@ The spec update confirmation SHALL provide clear visibility into changes before
|
||||
- **AND** display message: "Archive cancelled. No changes were made."
|
||||
- **AND** exit with non-zero status code
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Requirement: Error Conditions
|
||||
|
||||
The command SHALL handle various error conditions gracefully.
|
||||
@@ -144,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,20 +3,22 @@
|
||||
## 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 all active changes to provide a comprehensive overview.
|
||||
|
||||
#### Scenario: Scanning for changes
|
||||
|
||||
- **WHEN** `openspec list` is executed
|
||||
#### 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: Task Counting
|
||||
|
||||
The command SHALL accurately count task completion status using standard markdown checkbox patterns.
|
||||
@@ -30,37 +32,42 @@ 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 changes in a clear, readable table format with progress 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")
|
||||
- **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
|
||||
```
|
||||
#### 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: 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)
|
||||
|
||||
### Requirement: Empty State
|
||||
The command SHALL provide clear feedback when no items are present for the selected mode.
|
||||
|
||||
The command SHALL provide clear feedback when no active changes are present.
|
||||
|
||||
#### Scenario: Handling empty state
|
||||
|
||||
#### 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: Error Handling
|
||||
|
||||
The command SHALL gracefully handle missing files and directories with appropriate messages.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Core Requirements
|
||||
|
||||
## 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,6 +3,226 @@
|
||||
## 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.
|
||||
|
||||
#### Scenario: Following the structured spec format
|
||||
|
||||
- **WHEN** writing or updating OpenSpec specifications
|
||||
- **THEN** authors SHALL use `### Requirement: ...` followed by at least one `#### Scenario: ...` section
|
||||
|
||||
### Requirement: Project Structure
|
||||
|
||||
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes.
|
||||
|
||||
#### Scenario: Initializing project structure
|
||||
|
||||
- **WHEN** an OpenSpec project is initialized
|
||||
- **THEN** it SHALL have this structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
├── README.md # AI assistant instructions
|
||||
├── specs/ # Current deployed capabilities
|
||||
│ └── [capability]/ # Single, focused capability
|
||||
│ ├── spec.md # WHAT and WHY
|
||||
│ └── design.md # HOW (optional, for established patterns)
|
||||
└── changes/ # Proposed changes
|
||||
├── [change-name]/ # Descriptive change identifier
|
||||
│ ├── proposal.md # Why, what, and impact
|
||||
│ ├── tasks.md # Implementation checklist
|
||||
│ ├── design.md # Technical decisions (optional)
|
||||
│ └── specs/ # Complete future state
|
||||
│ └── [capability]/
|
||||
│ └── spec.md # Clean markdown (no diff syntax)
|
||||
└── archive/ # Completed changes
|
||||
└── YYYY-MM-DD-[name]/
|
||||
```
|
||||
|
||||
### Requirement: Structured Format for Behavioral Specs
|
||||
|
||||
Behavioral specifications SHALL use a structured format with consistent section headers and keywords to ensure visual consistency and parseability.
|
||||
|
||||
#### Scenario: Writing requirement sections
|
||||
|
||||
- **WHEN** documenting a requirement in a behavioral specification
|
||||
- **THEN** use a level-3 heading with format `### Requirement: [Name]`
|
||||
- **AND** immediately follow with a SHALL statement describing core behavior
|
||||
- **AND** keep requirement names descriptive and under 50 characters
|
||||
|
||||
#### Scenario: Documenting scenarios
|
||||
|
||||
- **WHEN** documenting specific behaviors or use cases
|
||||
- **THEN** use level-4 headings with format `#### Scenario: [Description]`
|
||||
- **AND** use bullet points with bold keywords for steps:
|
||||
- **GIVEN** for initial state (optional)
|
||||
- **WHEN** for conditions or triggers
|
||||
- **THEN** for expected outcomes
|
||||
- **AND** for additional outcomes or conditions
|
||||
|
||||
#### Scenario: Adding implementation details
|
||||
|
||||
- **WHEN** a step requires additional detail
|
||||
- **THEN** use sub-bullets under the main step
|
||||
- **AND** maintain consistent indentation
|
||||
- Sub-bullets provide examples or specifics
|
||||
- Keep sub-bullets concise
|
||||
|
||||
### Requirement: Header-Based Requirement Identification
|
||||
|
||||
Requirement headers SHALL serve as unique identifiers for programmatic matching between current specs and proposed changes.
|
||||
|
||||
#### Scenario: Matching requirements programmatically
|
||||
|
||||
- **WHEN** processing delta changes
|
||||
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
|
||||
- **AND** match using normalized headers: `normalize(header) = trim(header)`
|
||||
- **AND** compare headers with case-sensitive equality after normalization
|
||||
|
||||
#### Scenario: Handling requirement renames
|
||||
|
||||
- **WHEN** renaming a requirement
|
||||
- **THEN** use a special `## RENAMED Requirements` section
|
||||
- **AND** specify both old and new names explicitly:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
- **AND** if content also changes, include under MODIFIED using the NEW header
|
||||
|
||||
#### Scenario: Validating header uniqueness
|
||||
|
||||
- **WHEN** creating or modifying requirements
|
||||
- **THEN** ensure no duplicate headers exist within a spec
|
||||
- **AND** validation tools SHALL flag duplicate headers as errors
|
||||
|
||||
### Requirement: Change Storage Convention
|
||||
|
||||
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
|
||||
|
||||
#### Scenario: Creating change proposals with additions
|
||||
|
||||
- **WHEN** creating a change proposal that adds new requirements
|
||||
- **THEN** include only the new requirements under `## ADDED Requirements`
|
||||
- **AND** each requirement SHALL include its complete content
|
||||
- **AND** use the standard structured format for requirements and scenarios
|
||||
|
||||
#### Scenario: Creating change proposals with modifications
|
||||
|
||||
- **WHEN** creating a change proposal that modifies existing requirements
|
||||
- **THEN** include the modified requirements under `## MODIFIED Requirements`
|
||||
- **AND** use the same header text as in the current spec (normalized)
|
||||
- **AND** include the complete modified requirement (not a diff)
|
||||
- **AND** optionally annotate what changed with inline comments like `← (was X)`
|
||||
|
||||
#### Scenario: Creating change proposals with removals
|
||||
|
||||
- **WHEN** creating a change proposal that removes requirements
|
||||
- **THEN** list them under `## REMOVED Requirements`
|
||||
- **AND** use the normalized header text for identification
|
||||
- **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
|
||||
- Normalized header matching for requirement identification
|
||||
- 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.
|
||||
|
||||
#### Scenario: Archiving changes with deltas
|
||||
|
||||
- **WHEN** archiving a completed change
|
||||
- **THEN** the archive command SHALL:
|
||||
1. Parse RENAMED sections first and apply renames
|
||||
2. Parse REMOVED sections and remove by normalized header match
|
||||
3. Parse MODIFIED sections and replace by normalized header match (using new names if renamed)
|
||||
4. Parse ADDED sections and append new requirements
|
||||
- **AND** validate that all MODIFIED/REMOVED headers exist in current spec
|
||||
- **AND** validate that ADDED headers don't already exist
|
||||
- **AND** generate the updated spec in the main specs/ directory
|
||||
|
||||
#### Scenario: Handling conflicts during archive
|
||||
|
||||
- **WHEN** delta changes conflict with current spec state
|
||||
- **THEN** the archive command SHALL report specific conflicts
|
||||
- **AND** require manual resolution before proceeding
|
||||
- **AND** provide clear guidance on resolving conflicts
|
||||
|
||||
### Requirement: Proposal Format
|
||||
|
||||
Proposals SHALL explicitly document all changes with clear from/to comparisons.
|
||||
|
||||
#### Scenario: Documenting changes
|
||||
|
||||
- **WHEN** documenting what changes
|
||||
- **THEN** the proposal SHALL explicitly describe each change:
|
||||
|
||||
```markdown
|
||||
**[Section or Behavior Name]**
|
||||
- From: [current state/requirement]
|
||||
- To: [future state/requirement]
|
||||
- Reason: [why this change is needed]
|
||||
- Impact: [breaking/non-breaking, who's affected]
|
||||
```
|
||||
|
||||
This explicit format compensates for not having inline diffs and ensures reviewers understand exactly what will change.
|
||||
|
||||
### Requirement: Change Review
|
||||
|
||||
The system SHALL support multiple methods for reviewing proposed changes.
|
||||
|
||||
#### Scenario: Reviewing changes
|
||||
|
||||
- **WHEN** reviewing proposed changes
|
||||
- **THEN** reviewers can compare using:
|
||||
- GitHub PR diff view when changes are committed
|
||||
- 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
|
||||
|
||||
@@ -73,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
|
||||
@@ -132,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
|
||||
|
||||
+16
-5
@@ -94,12 +94,14 @@ program
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('List all active changes with their task status (DEPRECATED: use "openspec change list" instead)')
|
||||
.action(async () => {
|
||||
.description('List items (changes by default). Use --specs to list specs.')
|
||||
.option('--specs', 'List specs instead of changes')
|
||||
.option('--changes', 'List changes explicitly (default)')
|
||||
.action(async (options?: { specs?: boolean; changes?: boolean }) => {
|
||||
try {
|
||||
console.log('\x1b[33m%s\x1b[0m', 'Warning: The "openspec list" command is deprecated. Please use "openspec change list" instead.\n');
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute();
|
||||
const mode: 'changes' | 'specs' = options?.specs ? 'specs' : 'changes';
|
||||
await listCommand.execute('.', mode);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
@@ -112,6 +114,11 @@ const changeCmd = program
|
||||
.command('change')
|
||||
.description('Manage OpenSpec change proposals');
|
||||
|
||||
// Deprecation notice for noun-based commands
|
||||
changeCmd.hook('preAction', () => {
|
||||
console.error('Warning: The "openspec change ..." commands are deprecated. Prefer verb-first commands (e.g., "openspec list", "openspec validate --changes").');
|
||||
});
|
||||
|
||||
changeCmd
|
||||
.command('show [change-name]')
|
||||
.description('Show a change proposal in JSON or markdown format')
|
||||
@@ -131,11 +138,12 @@ changeCmd
|
||||
|
||||
changeCmd
|
||||
.command('list')
|
||||
.description('List all active changes')
|
||||
.description('List all active changes (DEPRECATED: use "openspec list" instead)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--long', 'Show id and title with counts')
|
||||
.action(async (options?: { json?: boolean; long?: boolean }) => {
|
||||
try {
|
||||
console.error('Warning: "openspec change list" is deprecated. Use "openspec list".');
|
||||
const changeCommand = new ChangeCommand();
|
||||
await changeCommand.list(options);
|
||||
} catch (error) {
|
||||
@@ -154,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}`));
|
||||
}
|
||||
}
|
||||
+60
-54
@@ -10,10 +10,65 @@ import { getSpecIds } from '../utils/item-discovery.js';
|
||||
|
||||
const SPECS_DIR = 'openspec/specs';
|
||||
|
||||
interface ShowOptions {
|
||||
json?: boolean;
|
||||
// JSON-only filters (raw-first text has no filters)
|
||||
requirements?: boolean;
|
||||
scenarios?: boolean; // --no-scenarios sets this to false (JSON only)
|
||||
requirement?: string; // JSON only
|
||||
noInteractive?: boolean;
|
||||
}
|
||||
|
||||
function parseSpecFromFile(specPath: string, specId: string): Spec {
|
||||
const content = readFileSync(specPath, 'utf-8');
|
||||
const parser = new MarkdownParser(content);
|
||||
return parser.parseSpec(specId);
|
||||
}
|
||||
|
||||
function validateRequirementIndex(spec: Spec, requirementOpt?: string): number | undefined {
|
||||
if (!requirementOpt) return undefined;
|
||||
const index = Number.parseInt(requirementOpt, 10);
|
||||
if (!Number.isInteger(index) || index < 1 || index > spec.requirements.length) {
|
||||
throw new Error(`Requirement ${requirementOpt} not found`);
|
||||
}
|
||||
return index - 1; // convert to 0-based
|
||||
}
|
||||
|
||||
function filterSpec(spec: Spec, options: ShowOptions): Spec {
|
||||
const requirementIndex = validateRequirementIndex(spec, options.requirement);
|
||||
const includeScenarios = options.scenarios !== false && !options.requirements;
|
||||
|
||||
const filteredRequirements = (requirementIndex !== undefined
|
||||
? [spec.requirements[requirementIndex]]
|
||||
: spec.requirements
|
||||
).map(req => ({
|
||||
text: req.text,
|
||||
scenarios: includeScenarios ? req.scenarios : [],
|
||||
}));
|
||||
|
||||
const metadata = spec.metadata ?? { version: '1.0.0', format: 'openspec' as const };
|
||||
|
||||
return {
|
||||
name: spec.name,
|
||||
overview: spec.overview,
|
||||
requirements: filteredRequirements,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the raw markdown content for a spec file without any formatting.
|
||||
* Raw-first behavior ensures text mode is a passthrough for deterministic output.
|
||||
*/
|
||||
function printSpecTextRaw(specPath: string): void {
|
||||
const content = readFileSync(specPath, 'utf-8');
|
||||
console.log(content);
|
||||
}
|
||||
|
||||
export class SpecCommand {
|
||||
private SPECS_DIR = 'openspec/specs';
|
||||
|
||||
async show(specId?: string, options: { json?: boolean; requirements?: boolean; scenarios?: boolean; requirement?: string; noInteractive?: boolean } = {}): Promise<void> {
|
||||
async show(specId?: string, options: ShowOptions = {}): Promise<void> {
|
||||
if (!specId) {
|
||||
const canPrompt = isInteractive(options?.noInteractive);
|
||||
const specIds = await getSpecIds();
|
||||
@@ -58,59 +113,10 @@ export function registerSpecCommand(rootProgram: typeof program) {
|
||||
.command('spec')
|
||||
.description('Manage and view OpenSpec specifications');
|
||||
|
||||
interface ShowOptions {
|
||||
json?: boolean;
|
||||
// JSON-only filters (raw-first text has no filters)
|
||||
requirements?: boolean;
|
||||
scenarios?: boolean; // --no-scenarios sets this to false (JSON only)
|
||||
requirement?: string; // JSON only
|
||||
}
|
||||
|
||||
function parseSpecFromFile(specPath: string, specId: string): Spec {
|
||||
const content = readFileSync(specPath, 'utf-8');
|
||||
const parser = new MarkdownParser(content);
|
||||
return parser.parseSpec(specId);
|
||||
}
|
||||
|
||||
function validateRequirementIndex(spec: Spec, requirementOpt?: string): number | undefined {
|
||||
if (!requirementOpt) return undefined;
|
||||
const index = Number.parseInt(requirementOpt, 10);
|
||||
if (!Number.isInteger(index) || index < 1 || index > spec.requirements.length) {
|
||||
throw new Error(`Requirement ${requirementOpt} not found`);
|
||||
}
|
||||
return index - 1; // convert to 0-based
|
||||
}
|
||||
|
||||
function filterSpec(spec: Spec, options: ShowOptions): Spec {
|
||||
const requirementIndex = validateRequirementIndex(spec, options.requirement);
|
||||
const includeScenarios = options.scenarios !== false && !options.requirements;
|
||||
|
||||
const filteredRequirements = (requirementIndex !== undefined
|
||||
? [spec.requirements[requirementIndex]]
|
||||
: spec.requirements
|
||||
).map(req => ({
|
||||
text: req.text,
|
||||
scenarios: includeScenarios ? req.scenarios : [],
|
||||
}));
|
||||
|
||||
const metadata = spec.metadata ?? { version: '1.0.0', format: 'openspec' as const };
|
||||
|
||||
return {
|
||||
name: spec.name,
|
||||
overview: spec.overview,
|
||||
requirements: filteredRequirements,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the raw markdown content for a spec file without any formatting.
|
||||
* Raw-first behavior ensures text mode is a passthrough for deterministic output.
|
||||
*/
|
||||
function printSpecTextRaw(specPath: string): void {
|
||||
const content = readFileSync(specPath, 'utf-8');
|
||||
console.log(content);
|
||||
}
|
||||
// Deprecation notice for noun-based commands
|
||||
specCommand.hook('preAction', () => {
|
||||
console.error('Warning: The "openspec spec ..." commands are deprecated. Prefer verb-first commands (e.g., "openspec show", "openspec validate --specs").');
|
||||
});
|
||||
|
||||
specCommand
|
||||
.command('show [spec-id]')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -160,9 +161,25 @@ export class ValidateCommand {
|
||||
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
||||
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
||||
}
|
||||
this.printNextSteps(type);
|
||||
}
|
||||
}
|
||||
|
||||
private printNextSteps(type: ItemType): void {
|
||||
const bullets: string[] = [];
|
||||
if (type === 'change') {
|
||||
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');
|
||||
} else {
|
||||
bullets.push('- Ensure spec includes ## Purpose and ## Requirements sections');
|
||||
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
||||
bullets.push('- Re-run with --json to see structured report');
|
||||
}
|
||||
console.error('Next steps:');
|
||||
bullets.forEach(b => console.error(` ${b}`));
|
||||
}
|
||||
|
||||
private async runBulkValidation(scope: { changes: boolean; specs: boolean }, opts: { strict: boolean; json: boolean; concurrency?: string }): Promise<void> {
|
||||
const spinner = !opts.json ? ora('Validating...').start() : undefined;
|
||||
const [changeIds, specIds] = await Promise.all([
|
||||
@@ -179,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;
|
||||
|
||||
+82
-38
@@ -1,6 +1,9 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { MarkdownParser } from './parsers/markdown-parser.js';
|
||||
|
||||
interface ChangeInfo {
|
||||
name: string;
|
||||
@@ -9,52 +12,93 @@ interface ChangeInfo {
|
||||
}
|
||||
|
||||
export class ListCommand {
|
||||
async execute(targetPath: string = '.'): Promise<void> {
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
||||
}
|
||||
async execute(targetPath: string = '.', mode: 'changes' | 'specs' = 'changes'): Promise<void> {
|
||||
if (mode === 'changes') {
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
||||
}
|
||||
|
||||
// Get all directories in changes (excluding archive)
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const changeDirs = entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
||||
.map(entry => entry.name);
|
||||
// Get all directories in changes (excluding archive)
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const changeDirs = entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
||||
.map(entry => entry.name);
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
console.log('No active changes found.');
|
||||
if (changeDirs.length === 0) {
|
||||
console.log('No active changes found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect information about each change
|
||||
const changes: ChangeInfo[] = [];
|
||||
|
||||
for (const changeDir of changeDirs) {
|
||||
const progress = await getTaskProgressForChange(changesDir, changeDir);
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks: progress.completed,
|
||||
totalTasks: progress.total
|
||||
});
|
||||
}
|
||||
|
||||
// Sort alphabetically by name
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Display results
|
||||
console.log('Changes:');
|
||||
const padding = ' ';
|
||||
const nameWidth = Math.max(...changes.map(c => c.name.length));
|
||||
for (const change of changes) {
|
||||
const paddedName = change.name.padEnd(nameWidth);
|
||||
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
|
||||
console.log(`${padding}${paddedName} ${status}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect information about each change
|
||||
const changes: ChangeInfo[] = [];
|
||||
|
||||
for (const changeDir of changeDirs) {
|
||||
const progress = await getTaskProgressForChange(changesDir, changeDir);
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks: progress.completed,
|
||||
totalTasks: progress.total
|
||||
});
|
||||
// specs mode
|
||||
const specsDir = path.join(targetPath, 'openspec', 'specs');
|
||||
try {
|
||||
await fs.access(specsDir);
|
||||
} catch {
|
||||
console.log('No specs found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort alphabetically by name
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
||||
const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
||||
if (specDirs.length === 0) {
|
||||
console.log('No specs found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Display results
|
||||
console.log('Changes:');
|
||||
for (const change of changes) {
|
||||
const padding = ' ';
|
||||
const nameWidth = Math.max(...changes.map(c => c.name.length));
|
||||
const paddedName = change.name.padEnd(nameWidth);
|
||||
|
||||
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
|
||||
|
||||
console.log(`${padding}${paddedName} ${status}`);
|
||||
type SpecInfo = { id: string; requirementCount: number };
|
||||
const specs: SpecInfo[] = [];
|
||||
for (const id of specDirs) {
|
||||
const specPath = join(specsDir, id, 'spec.md');
|
||||
try {
|
||||
const content = readFileSync(specPath, 'utf-8');
|
||||
const parser = new MarkdownParser(content);
|
||||
const spec = parser.parseSpec(id);
|
||||
specs.push({ id, requirementCount: spec.requirements.length });
|
||||
} catch {
|
||||
// If spec cannot be read or parsed, include with 0 count
|
||||
specs.push({ id, requirementCount: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
specs.sort((a, b) => a.id.localeCompare(b.id));
|
||||
console.log('Specs:');
|
||||
const padding = ' ';
|
||||
const nameWidth = Math.max(...specs.map(s => s.id.length));
|
||||
for (const spec of specs) {
|
||||
const padded = spec.id.padEnd(nameWidth);
|
||||
console.log(`${padding}${padded} requirements ${spec.requirementCount}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,4 +35,14 @@ export const VALIDATION_MESSAGES = {
|
||||
REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Consider breaking it down.`,
|
||||
DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
|
||||
DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
|
||||
|
||||
// Guidance snippets (appended to primary messages for remediation)
|
||||
GUIDE_NO_DELTAS:
|
||||
'No deltas found. Ensure your change has a specs/ directory with .md files using delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
|
||||
GUIDE_MISSING_SPEC_SECTIONS:
|
||||
'Missing required sections. Expected headers: "## Purpose" and "## Requirements". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...',
|
||||
GUIDE_MISSING_CHANGE_SECTIONS:
|
||||
'Missing required sections. Expected headers: "## Why" and "## What Changes". Ensure deltas are documented in specs/ using delta headers.',
|
||||
GUIDE_SCENARIO_FORMAT:
|
||||
'Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...',
|
||||
} as const;
|
||||
@@ -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;
|
||||
@@ -20,11 +21,10 @@ export class Validator {
|
||||
|
||||
async validateSpec(filePath: string): Promise<ValidationReport> {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
const specName = this.extractNameFromPath(filePath);
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const parser = new MarkdownParser(content);
|
||||
const specName = this.extractNameFromPath(filePath);
|
||||
|
||||
const spec = parser.parseSpec(specName);
|
||||
|
||||
@@ -37,22 +37,44 @@ export class Validator {
|
||||
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: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: enriched,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const changeName = this.extractNameFromPath(filePath);
|
||||
const changeDir = path.dirname(filePath);
|
||||
const parser = new ChangeParser(content, changeDir);
|
||||
|
||||
@@ -67,22 +89,172 @@ export class Validator {
|
||||
issues.push(...this.applyChangeRules(change, content));
|
||||
|
||||
} catch (error) {
|
||||
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
const enriched = this.enrichTopLevelError(changeName, baseMessage);
|
||||
issues.push({
|
||||
level: 'ERROR',
|
||||
path: 'file',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: enriched,
|
||||
});
|
||||
}
|
||||
|
||||
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 => ({
|
||||
level: 'ERROR' as ValidationLevel,
|
||||
path: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
return error.issues.map(err => {
|
||||
let message = err.message;
|
||||
if (message === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
|
||||
message = `${message}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
|
||||
}
|
||||
return {
|
||||
level: 'ERROR' as ValidationLevel,
|
||||
path: err.path.join('.'),
|
||||
message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private applySpecRules(spec: Spec, content: string): ValidationIssue[] {
|
||||
@@ -109,7 +281,7 @@ export class Validator {
|
||||
issues.push({
|
||||
level: 'WARNING',
|
||||
path: `requirements[${index}].scenarios`,
|
||||
message: 'Requirement has no scenarios',
|
||||
message: `${VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS}. ${VALIDATION_MESSAGES.GUIDE_SCENARIO_FORMAT}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -144,6 +316,20 @@ export class Validator {
|
||||
return issues;
|
||||
}
|
||||
|
||||
private enrichTopLevelError(itemId: string, baseMessage: string): string {
|
||||
const msg = baseMessage.trim();
|
||||
if (msg === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
|
||||
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
|
||||
}
|
||||
if (msg.includes('Spec must have a Purpose section') || msg.includes('Spec must have a Requirements section')) {
|
||||
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_SPEC_SECTIONS}`;
|
||||
}
|
||||
if (msg.includes('Change must have a Why section') || msg.includes('Change must have a What Changes section')) {
|
||||
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_CHANGE_SECTIONS}`;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
private extractNameFromPath(filePath: string): string {
|
||||
const parts = filePath.split('/');
|
||||
|
||||
@@ -184,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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
describe('validate command enriched human output', () => {
|
||||
const projectRoot = process.cwd();
|
||||
const testDir = path.join(projectRoot, 'test-validate-enriched-tmp');
|
||||
const changesDir = path.join(testDir, 'openspec', 'changes');
|
||||
const bin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
// Build once so the bin can resolve dist
|
||||
try { execSync('pnpm -s build', { stdio: 'pipe' }); } catch {}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('prints Next steps footer and guidance on invalid change', () => {
|
||||
const changeContent = `# Test Change\n\n## Why\nThis is a sufficiently long explanation to pass the why length requirement for validation purposes.\n\n## What Changes\nThere are changes proposed, but no delta specs provided yet.`;
|
||||
const changeId = 'c-next-steps';
|
||||
const changePath = path.join(changesDir, changeId);
|
||||
execSync(`mkdir -p ${changePath}`);
|
||||
execSync(`bash -lc "cat > ${path.join(changePath, 'proposal.md')} <<'EOF'\n${changeContent}\nEOF"`);
|
||||
|
||||
const originalCwd = process.cwd();
|
||||
try {
|
||||
process.chdir(testDir);
|
||||
let code = 0;
|
||||
let stderr = '';
|
||||
try {
|
||||
execSync(`node ${bin} change validate ${changeId}`, { encoding: 'utf-8', stdio: 'pipe' });
|
||||
} catch (e: any) {
|
||||
code = e?.status ?? 1;
|
||||
stderr = e?.stderr?.toString?.() ?? '';
|
||||
}
|
||||
expect(code).not.toBe(0);
|
||||
expect(stderr).toContain('has issues');
|
||||
expect(stderr).toContain('Next steps:');
|
||||
expect(stderr).toContain('openspec change show');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('ListCommand', () => {
|
||||
it('should handle missing openspec/changes directory', async () => {
|
||||
const listCommand = new ListCommand();
|
||||
|
||||
await expect(listCommand.execute(tempDir)).rejects.toThrow(
|
||||
await expect(listCommand.execute(tempDir, 'changes')).rejects.toThrow(
|
||||
"No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
);
|
||||
});
|
||||
@@ -44,7 +44,7 @@ describe('ListCommand', () => {
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
await listCommand.execute(tempDir, 'changes');
|
||||
|
||||
expect(logOutput).toEqual(['No active changes found.']);
|
||||
});
|
||||
@@ -61,7 +61,7 @@ describe('ListCommand', () => {
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
await listCommand.execute(tempDir, 'changes');
|
||||
|
||||
expect(logOutput).toContain('Changes:');
|
||||
expect(logOutput.some(line => line.includes('my-change'))).toBe(true);
|
||||
@@ -85,7 +85,7 @@ Regular text that should be ignored
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
await listCommand.execute(tempDir, 'changes');
|
||||
|
||||
expect(logOutput.some(line => line.includes('2/5 tasks'))).toBe(true);
|
||||
});
|
||||
@@ -100,7 +100,7 @@ Regular text that should be ignored
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
await listCommand.execute(tempDir, 'changes');
|
||||
|
||||
expect(logOutput.some(line => line.includes('✓ Complete'))).toBe(true);
|
||||
});
|
||||
@@ -110,7 +110,7 @@ Regular text that should be ignored
|
||||
await fs.mkdir(path.join(changesDir, 'no-tasks'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
await listCommand.execute(tempDir, 'changes');
|
||||
|
||||
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { Validator } from '../../src/core/validation/validator.js';
|
||||
|
||||
describe('Validator enriched messages', () => {
|
||||
const testDir = path.join(process.cwd(), 'test-validation-enriched-tmp');
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('adds guidance for no deltas in change', async () => {
|
||||
const changeContent = `# Test Change
|
||||
|
||||
## Why
|
||||
This is a sufficiently long explanation to pass the why length requirement for validation purposes.
|
||||
|
||||
## What Changes
|
||||
There are changes proposed, but no delta specs provided yet.`;
|
||||
const changePath = path.join(testDir, 'proposal.md');
|
||||
await fs.writeFile(changePath, changeContent);
|
||||
|
||||
const validator = new Validator();
|
||||
const report = await validator.validateChange(changePath);
|
||||
expect(report.valid).toBe(false);
|
||||
const msg = report.issues.map(i => i.message).join('\n');
|
||||
expect(msg).toContain('Change must have at least one delta');
|
||||
expect(msg).toContain('Ensure your change has a specs/ directory');
|
||||
expect(msg).toContain('## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
||||
});
|
||||
|
||||
it('adds guidance when spec missing Purpose/Requirements', async () => {
|
||||
const specContent = `# Test Spec\n\n## Requirements\n\n### Requirement: Foo\nFoo SHALL ...\n\n#### Scenario: Bar\nWhen...`;
|
||||
const specPath = path.join(testDir, 'spec.md');
|
||||
await fs.writeFile(specPath, specContent);
|
||||
|
||||
const validator = new Validator();
|
||||
const report = await validator.validateSpec(specPath);
|
||||
expect(report.valid).toBe(false);
|
||||
const msg = report.issues.map(i => i.message).join('\n');
|
||||
expect(msg).toContain('Spec must have a Purpose section');
|
||||
expect(msg).toContain('Expected headers: "## Purpose" and "## Requirements"');
|
||||
});
|
||||
|
||||
it('warns with scenario conversion template when missing scenarios', async () => {
|
||||
const specContent = `# Test Spec
|
||||
|
||||
## Purpose
|
||||
This is a sufficiently long purpose section to avoid warnings about brevity.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Foo SHALL be described
|
||||
Text of requirement
|
||||
`;
|
||||
const specPath = path.join(testDir, 'spec.md');
|
||||
await fs.writeFile(specPath, specContent);
|
||||
|
||||
const validator = new Validator();
|
||||
const report = await validator.validateSpec(specPath);
|
||||
expect(report.valid).toBe(false);
|
||||
const warn = report.issues.find(i => i.path.includes('requirements[0].scenarios'));
|
||||
expect(warn?.message).toContain('Requirement must have at least one scenario');
|
||||
expect(warn?.message).toContain('Scenarios must use level-4 headers');
|
||||
expect(warn?.message).toContain('#### Scenario:');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user