-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: Add hierarchical specs change proposal #839
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| schema: spec-driven | ||
| created: 2026-03-13 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| ## Context | ||
|
|
||
| OpenSpec currently stores specs in a flat directory structure at `openspec/specs/<spec-id>/spec.md`. Spec IDs are simple strings (e.g., `cli-show`, `schema-resolution`). The `getSpecIds()` function in `item-discovery.ts` reads only one level deep, and all path construction uses `path.join(SPECS_DIR, specId, 'spec.md')`. | ||
|
|
||
| As the project has grown to 38 specs, naming conventions like `cli-*`, `schema-*` have emerged organically — encoding hierarchy in flat names. This change introduces proper directory nesting so specs can be organized as `cli/show`, `schema/resolution`, etc. | ||
|
|
||
| ## Goals / Non-Goals | ||
|
|
||
| **Goals:** | ||
| - Support arbitrary nesting depth for specs (e.g., `domain/project/feature`) | ||
| - Make spec IDs path-based, using `/` as the separator | ||
| - Maintain full backward compatibility with existing flat spec IDs | ||
| - Update all CLI commands to work with hierarchical spec IDs | ||
| - Support subtree operations (e.g., listing all specs under `cli/`) | ||
|
|
||
| **Non-Goals:** | ||
| - Automatic migration of existing flat specs to hierarchical structure — users migrate at their own pace | ||
| - Cross-references or symbolic links between specs | ||
| - Spec inheritance or composition across hierarchy levels | ||
| - Namespace-level metadata or configuration (e.g., `cli/.openspec.yaml`) | ||
|
|
||
| ## Decisions | ||
|
|
||
| ### Decision 1: Spec IDs use forward slash as separator, regardless of OS | ||
|
|
||
| Spec IDs use `/` as the canonical separator (e.g., `cli/show`), even on Windows. Internally, `path.join` is used for filesystem operations, but IDs are always stored and displayed with `/`. | ||
|
|
||
| **Why**: Consistent cross-platform behavior. Spec IDs appear in change files, JSON output, and documentation — they must be portable. This matches how Go import paths and npm package scopes work. | ||
|
|
||
| **Alternative considered**: Using the OS path separator — rejected because spec IDs would differ across platforms, breaking change portability. | ||
|
|
||
| ### Decision 2: Recursive discovery with `spec.md` as the leaf marker | ||
|
|
||
| `getSpecIds()` walks the directory tree recursively. Any directory containing `spec.md` is a spec — its ID is the relative path from `openspec/specs/` to that directory. Directories without `spec.md` are treated as organizational containers. | ||
|
|
||
| **Why**: Simple, unambiguous detection. No configuration files needed at intermediate directories. The existing convention (`spec.md` = spec exists) naturally extends to nested structures. | ||
|
|
||
| **Alternative considered**: Requiring a manifest file listing specs — rejected as it adds maintenance burden and a sync problem. | ||
|
|
||
| ### Decision 3: Flat and hierarchical specs coexist | ||
|
|
||
| Both `openspec/specs/cli-show/spec.md` (flat) and `openspec/specs/cli/show/spec.md` (hierarchical) are valid. They are different specs with different IDs (`cli-show` vs `cli/show`). | ||
|
|
||
| **Why**: Zero-migration-cost adoption. Teams can gradually reorganize without a flag day. Existing tooling and change references continue to work. | ||
|
|
||
| **Alternative considered**: Deprecating flat structure — rejected as it forces migration and breaks existing changes/archives. | ||
|
|
||
| ### Decision 4: Subtree filtering uses prefix matching on spec IDs | ||
|
|
||
| `openspec spec list cli/` returns all specs whose ID starts with `cli/`. This is a simple string prefix match on the canonical `/`-separated ID. | ||
|
|
||
| **Why**: Intuitive UX — `cli/` means "everything under cli". No glob syntax needed for the common case. | ||
|
|
||
| ### Decision 5: Delta specs in changes mirror the hierarchy | ||
|
|
||
| Change delta specs at `changes/<name>/specs/` use the same hierarchy. For example, modifying spec `cli/show` means creating `changes/<name>/specs/cli/show/spec.md`. | ||
|
|
||
| **Why**: Consistent mental model. The change's `specs/` directory is a mirror of the main `openspec/specs/` structure. The archive command can use the same relative path logic for both. | ||
|
|
||
| ### Decision 6: Fuzzy matching extends to hierarchical IDs | ||
|
|
||
| The existing Levenshtein-based suggestion system works on the full path-based ID string. Additionally, partial path matching is supported — typing `show` suggests `cli/show` if it exists. | ||
|
|
||
| **Why**: Discoverability. Users may not know the full path. Matching on the leaf segment (last component) helps find specs without knowing the full hierarchy. | ||
|
|
||
| ## Risks / Trade-offs | ||
|
|
||
| **[Risk] Ambiguity between flat and nested IDs** → Flat ID `cli-show` and nested ID `cli/show` are distinct specs. If both exist, commands resolve them independently. Documentation should clarify naming conventions to avoid confusion. | ||
|
|
||
| **[Risk] Deep nesting becomes unwieldy** → No technical limit on depth, but deeply nested IDs (`a/b/c/d/e/spec.md`) are cumbersome to type. Mitigation: document recommended max depth of 3 levels; fuzzy matching reduces typing burden. | ||
|
|
||
| **[Risk] Performance with large spec trees** → Recursive directory walking is slower than single-level readdir. Mitigation: `fast-glob` (already a dependency) can handle thousands of entries efficiently. Benchmark if >500 specs. | ||
|
|
||
| **[Risk] Cross-platform path handling bugs** → Mixing `/` in IDs with OS-specific `path.join` is error-prone. Mitigation: centralize ID↔path conversion in a single utility (`specIdToPath` / `pathToSpecId`), and add Windows path tests as per existing config rules. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| ## Why | ||
|
|
||
| OpenSpec stores all specs in a flat structure under `openspec/specs/`. As projects grow, this becomes difficult to organize and navigate. Teams working on different domains or features resort to long, prefixed names (e.g., `cli-show`, `cli-validate`, `schema-fork-command`) to avoid collisions and convey hierarchy through naming conventions alone. A proper hierarchical structure would make specs more discoverable, reduce naming collisions, and let teams organize specs by domain, project, or feature. | ||
|
|
||
| ## What Changes | ||
|
|
||
| - Spec IDs become path-based (e.g., `cli/show` instead of `cli-show`), supporting arbitrary nesting depth | ||
| - `getSpecIds()` in `item-discovery.ts` becomes recursive, discovering specs at any depth | ||
| - All CLI commands that resolve spec IDs (`spec show`, `spec list`, `spec validate`, `show`, `validate`, `list`, `view`) accept path-based spec IDs | ||
| - Path construction throughout the codebase changes from `join(SPECS_DIR, specId, 'spec.md')` to handle `/`-separated IDs as nested directories | ||
| - `spec list` gains the ability to filter by subtree (e.g., `openspec spec list cli/` shows only CLI specs) | ||
| - Delta specs in changes mirror the hierarchical structure | ||
| - **COMPATIBILITY**: Existing flat spec IDs remain valid — no migration required. Tools that treat spec IDs as opaque strings need no changes. Tools that parse or construct spec ID structure (e.g., splitting on `-` to infer hierarchy) must be updated to handle `/`-separated IDs. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### New Capabilities | ||
| - `hierarchical-spec-discovery`: Recursive spec discovery that finds `spec.md` files at any nesting depth under `openspec/specs/`, deriving spec IDs from relative directory paths | ||
| - `hierarchical-spec-resolution`: Path-based spec ID resolution, allowing specs to be referenced as `domain/project/feature` with subtree filtering and disambiguation support | ||
|
|
||
| ### Modified Capabilities | ||
| - `cli-spec`: Spec commands (`show`, `list`, `validate`) must accept path-based spec IDs and support subtree listing | ||
| - `cli-show`: Show command must resolve hierarchical spec IDs | ||
| - `cli-validate`: Validate command must resolve hierarchical spec IDs | ||
| - `cli-list`: List command must display specs with their full hierarchical paths and support subtree filtering | ||
| - `cli-view`: View/dashboard must display specs organized by hierarchy | ||
| - `cli-archive`: Archive must handle delta specs in nested directory structures | ||
|
|
||
| ## Impact | ||
|
|
||
| - **Core**: `item-discovery.ts` (`getSpecIds`), path construction in `spec.ts`, `show.ts`, `validate.ts` | ||
| - **Display**: `list.ts` and `view.ts` need tree-aware rendering | ||
| - **Archive**: `specs-apply.ts` and `archive.ts` need to handle nested delta spec paths | ||
| - **Change specs**: Delta specs within changes (`changes/<name>/specs/`) mirror the hierarchy | ||
| - **Tests**: All spec-related tests need updating for nested path scenarios | ||
| - **Cross-platform**: Path handling must use `path.join`/`path.posix` correctly — spec IDs use `/` as separator regardless of OS |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| ## MODIFIED Requirements | ||
|
|
||
| ### Requirement: Spec Update Process | ||
|
|
||
| Before moving the change to archive, the command SHALL apply delta changes to main specs to reflect the deployed reality. | ||
|
|
||
| #### Scenario: Applying delta changes | ||
|
|
||
| - **WHEN** archiving a change with delta-based specs | ||
| - **THEN** recursively discover delta specs within the change's `specs/` directory | ||
| - **AND** parse and apply delta changes as defined in openspec-conventions | ||
| - **AND** validate all operations before applying | ||
| - **AND** create nested directories under `openspec/specs/` as needed for new hierarchical specs | ||
|
|
||
| #### Scenario: Applying hierarchical delta specs | ||
|
|
||
| - **WHEN** a change contains delta spec at `changes/<name>/specs/cli/show/spec.md` | ||
| - **THEN** apply the delta to `openspec/specs/cli/show/spec.md` | ||
| - **AND** create intermediate directories (`cli/`) if they do not exist | ||
|
|
||
| #### Scenario: Validating delta changes | ||
|
|
||
| - **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 | ||
|
|
||
| The spec update confirmation SHALL provide clear visibility into changes before they are applied. | ||
|
|
||
| #### Scenario: Displaying confirmation with hierarchical paths | ||
|
|
||
| - **WHEN** prompting for confirmation | ||
| - **THEN** display a clear summary showing: | ||
| - Which specs will be created (new capabilities) with full hierarchical paths | ||
| - Which specs will be updated (existing capabilities) with full hierarchical paths | ||
| - The source path for each spec | ||
| - **AND** format the confirmation prompt as: | ||
| ``` | ||
| The following specs will be updated: | ||
|
|
||
| NEW specs to be created: | ||
| - cli/archive (from changes/add-archive-command/specs/cli/archive/spec.md) | ||
|
|
||
| EXISTING specs to be updated: | ||
| - cli/init (from changes/update-init-command/specs/cli/init/spec.md) | ||
|
|
||
| Update 2 specs and archive 'add-archive-command'? [y/N]: | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| ## 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** recursively scan the `openspec/specs/` directory tree for capabilities at any depth | ||
| - **AND** read each capability's `spec.md` | ||
| - **AND** parse requirements to compute requirement counts | ||
|
|
||
| #### Scenario: Scanning for specs in subtree | ||
| - **WHEN** `openspec list --specs cli/` is executed | ||
| - **THEN** recursively scan only specs whose ID starts with `cli/` at a segment boundary (i.e., the character after the prefix must be a path separator or the prefix must end with `/`) | ||
| - **AND** display them with their full hierarchical IDs | ||
| - **AND** `cli/` matches `cli/show` and `cli/bar/baz` but NOT `client/foo` | ||
|
|
||
| ### 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 (full hierarchical path, e.g., `cli/show` or `cli-show`) | ||
| - Requirement count (e.g., "requirements 12") | ||
|
|
||
|
Comment on lines
+25
to
+38
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Table-only output requirement conflicts with machine-readable JSON mode. This section says output “SHALL” be table format, but the command surface includes 🤖 Prompt for AI Agents |
||
| ### Requirement: Sorting | ||
|
|
||
| The command SHALL maintain consistent ordering of items for predictable output. | ||
|
|
||
| #### Scenario: Ordering changes | ||
|
|
||
| - **WHEN** displaying multiple changes | ||
| - **THEN** sort them in alphabetical order by change name | ||
|
|
||
| #### Scenario: Ordering specs | ||
|
|
||
| - **WHEN** displaying multiple specs | ||
| - **THEN** sort them in alphabetical order by full spec ID | ||
| - **AND** hierarchical IDs sort naturally (e.g., `cli/archive` before `cli/show`) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| ## MODIFIED 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, including hierarchical spec IDs | ||
| - **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: Direct hierarchical spec display | ||
|
|
||
| - **WHEN** executing `openspec show cli/show` | ||
| - **THEN** detect that `cli/show` is a hierarchical spec ID | ||
| - **AND** resolve it at `openspec/specs/cli/show/spec.md` | ||
| - **AND** display the spec content | ||
|
|
||
| #### 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 including hierarchical specs | ||
|
|
||
| #### 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) | ||
| - **AND** support hierarchical spec IDs (e.g., `openspec show --type spec cli/show`) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| ## MODIFIED Requirements | ||
|
|
||
| ### 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** recursively scan the openspec/specs directory tree | ||
| - **AND** return list of all available capabilities with their full hierarchical IDs | ||
| - **AND** support JSON output with `--json` flag | ||
|
|
||
| #### Scenario: List specs in subtree | ||
|
|
||
| - **WHEN** executing `openspec spec list cli/` | ||
| - **THEN** return only specs whose ID starts with `cli/` | ||
| - **AND** display them with their full hierarchical IDs | ||
|
|
||
| #### Scenario: Show hierarchical spec | ||
|
|
||
| - **WHEN** executing `openspec spec show cli/show` | ||
| - **THEN** resolve the spec at `openspec/specs/cli/show/spec.md` | ||
| - **AND** display the spec content | ||
|
|
||
| #### 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 | ||
|
|
||
| #### Scenario: Validate hierarchical spec | ||
|
|
||
| - **WHEN** executing `openspec spec validate cli/show` | ||
| - **THEN** resolve the spec at `openspec/specs/cli/show/spec.md` | ||
| - **AND** validate against Zod schema | ||
| - **AND** report any structural issues | ||
|
|
||
| ### 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, including hierarchical IDs | ||
| - **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: 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, including hierarchical IDs | ||
| - **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 |
Uh oh!
There was an error while loading. Please reload this page.