Skip to content

Commit 4f4af57

Browse files
authored
feat: add change creation utilities (#408)
* proposal: add change manager - extract + new functionality Slice 2 of the artifact tracker POC. Creates ChangeManager module that: **Extracts existing functionality:** - `listChanges()` from ListCommand + ChangeCommand.getActiveChanges() - `changeExists()` from inline fs.access() checks - `getChangePath()` from inline path.join() calls - `isInitialized()` from ListCommand directory check **Adds new functionality:** - `createChange(name, description?)` - create change directory + README - `validateName(name)` - enforce kebab-case naming **Refactors CLI commands to be thin wrappers:** - ListCommand delegates to ChangeManager - ChangeCommand delegates to ChangeManager Also updates docs/artifact_poc.md to reflect XDG decisions from Slice 1. * proposal: simplify to utility functions only Remove extraction/refactor scope. Just add: - createChange(projectRoot, name, description?) - validateChangeName(name) Simple utility functions in src/utils/change-utils.ts. No class, no abstraction layer. * docs: update artifact_poc.md for simplified Slice 2 - Rename ChangeManager to change-utils (simple utility functions) - Remove extracted methods (listChanges, getChangePath, etc.) - Keep only new functionality: createChange(), validateChangeName() - Update component diagram and summary table * docs: clarify existing vs new functionality in artifact_poc.md Audit artifact_poc.md against existing codebase to mark what already exists vs what's genuinely new: - Slice 4 CLI table: Added Status column (NEW/EXISTS) - Added "Existing CLI commands" section listing what's not in scope - Updated Implementation Order Slice 4 with explicit new vs existing - Summary table: Updated status + added "What already exists" section Key finding: The document was already well-simplified. Only truly new functionality (createChange, validateChangeName, InstructionLoader, artifact graph CLI commands) is proposed. * rename: change-manager -> change-creation capability The capability name "change-manager" implied a manager class abstraction, but the simplified proposal uses only utility functions. Renamed to "change-creation" to accurately reflect what the capability provides. * feat: implement change creation utilities Add createChange() and validateChangeName() functions for programmatic change directory creation with kebab-case validation. - createChange(projectRoot, name) creates openspec/changes/<name>/ - validateChangeName() enforces kebab-case naming conventions - Comprehensive test coverage (21 tests) * chore: archive add-change-manager change Move change to archive and create change-creation spec with requirements for createChange() and validateChangeName(). * docs: update change-creation spec purpose
1 parent 9822576 commit 4f4af57

File tree

9 files changed

+760
-139
lines changed

9 files changed

+760
-139
lines changed

docs/artifact_poc.md

Lines changed: 200 additions & 137 deletions
Large diffs are not rendered by default.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
## Context
2+
3+
This is Slice 2 of the artifact tracker POC. The goal is to provide utilities for creating change directories programmatically.
4+
5+
**Current state:** No programmatic way to create changes. Users must manually create directories.
6+
7+
**Proposed state:** Utility functions for change creation with name validation.
8+
9+
## Goals / Non-Goals
10+
11+
### Goals
12+
- **Add** `createChange()` function to create change directories
13+
- **Add** `validateChangeName()` function for kebab-case validation
14+
- **Enable** automation (Claude commands, scripts) to create changes
15+
16+
### Non-Goals
17+
- Refactor existing CLI commands (they work fine)
18+
- Create abstraction layers or manager classes
19+
- Change how `ListCommand` or `ChangeCommand` work
20+
21+
## Decisions
22+
23+
### Decision 1: Simple Utility Functions
24+
25+
**Choice**: Add functions to `src/utils/change-utils.ts` - no class.
26+
27+
```typescript
28+
// src/utils/change-utils.ts
29+
30+
export function validateChangeName(name: string): { valid: boolean; error?: string }
31+
32+
export async function createChange(
33+
projectRoot: string,
34+
name: string
35+
): Promise<void>
36+
```
37+
38+
**Why**:
39+
- Simple, no abstraction overhead
40+
- Easy to test
41+
- Easy to import where needed
42+
- Matches existing utility patterns in `src/utils/`
43+
44+
**Alternatives considered**:
45+
- ChangeManager class: Rejected - over-engineered for 2 functions
46+
- Add to existing command: Rejected - mixes CLI with reusable logic
47+
48+
### Decision 2: Kebab-Case Validation Pattern
49+
50+
**Choice**: Validate names with `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`
51+
52+
Valid: `add-auth`, `refactor-db`, `add-feature-2`, `refactor`
53+
Invalid: `Add-Auth`, `add auth`, `add_auth`, `-add-auth`, `add-auth-`, `add--auth`
54+
55+
**Why**:
56+
- Filesystem-safe (no special characters)
57+
- URL-safe (for future web UI)
58+
- Consistent with existing change naming in repo
59+
60+
## File Changes
61+
62+
### New Files
63+
- `src/utils/change-utils.ts` - Utility functions
64+
- `src/utils/change-utils.test.ts` - Unit tests
65+
66+
### Modified Files
67+
- None
68+
69+
## Risks / Trade-offs
70+
71+
| Risk | Mitigation |
72+
|------|------------|
73+
| Function might not cover all use cases | Start simple, extend if needed |
74+
| Naming conflicts with future work | Using clear, specific function names |
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
## Why
2+
3+
There's no programmatic way to create a new change directory. Users must manually:
4+
1. Create `openspec/changes/<name>/` directory
5+
2. Create a `proposal.md` file
6+
3. Hope they got the naming right
7+
8+
This is error-prone and blocks automation (e.g., Claude commands, scripts).
9+
10+
**This proposal adds:**
11+
1. `createChange(projectRoot, name)` - Create change directories programmatically
12+
2. `validateChangeName(name)` - Enforce kebab-case naming conventions
13+
14+
## What Changes
15+
16+
### New Utilities
17+
18+
| Function | Description |
19+
|----------|-------------|
20+
| `createChange(projectRoot, name)` | Creates `openspec/changes/<name>/` directory |
21+
| `validateChangeName(name)` | Returns `{ valid: boolean; error?: string }` |
22+
23+
### Name Validation Rules
24+
25+
Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`
26+
27+
| Valid | Invalid |
28+
|-------|---------|
29+
| `add-auth` | `Add-Auth` (uppercase) |
30+
| `refactor-db` | `add auth` (spaces) |
31+
| `add-feature-2` | `add_auth` (underscores) |
32+
| `refactor` | `-add-auth` (leading hyphen) |
33+
34+
### Location
35+
36+
New file: `src/utils/change-utils.ts`
37+
38+
Simple utility functions - no class, no abstraction layer.
39+
40+
## Impact
41+
42+
- **Affected specs**: None
43+
- **Affected code**: None (new utilities only)
44+
- **New files**: `src/utils/change-utils.ts`
45+
- **Breaking changes**: None
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Change Creation
4+
The system SHALL provide a function to create new change directories programmatically.
5+
6+
#### Scenario: Create change
7+
- **WHEN** `createChange(projectRoot, 'add-auth')` is called
8+
- **THEN** the system creates `openspec/changes/add-auth/` directory
9+
10+
#### Scenario: Duplicate change rejected
11+
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and `openspec/changes/add-auth/` already exists
12+
- **THEN** the system throws an error indicating the change already exists
13+
14+
#### Scenario: Creates parent directories if needed
15+
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and `openspec/changes/` does not exist
16+
- **THEN** the system creates the full path including parent directories
17+
18+
#### Scenario: Invalid change name rejected
19+
- **WHEN** `createChange(projectRoot, 'Add Auth')` is called with an invalid name
20+
- **THEN** the system throws a validation error
21+
22+
### Requirement: Change Name Validation
23+
The system SHALL validate change names follow kebab-case conventions.
24+
25+
#### Scenario: Valid kebab-case name accepted
26+
- **WHEN** a change name like `add-user-auth` is validated
27+
- **THEN** validation returns `{ valid: true }`
28+
29+
#### Scenario: Numeric suffixes accepted
30+
- **WHEN** a change name like `add-feature-2` is validated
31+
- **THEN** validation returns `{ valid: true }`
32+
33+
#### Scenario: Single word accepted
34+
- **WHEN** a change name like `refactor` is validated
35+
- **THEN** validation returns `{ valid: true }`
36+
37+
#### Scenario: Uppercase characters rejected
38+
- **WHEN** a change name like `Add-Auth` is validated
39+
- **THEN** validation returns `{ valid: false, error: "..." }`
40+
41+
#### Scenario: Spaces rejected
42+
- **WHEN** a change name like `add auth` is validated
43+
- **THEN** validation returns `{ valid: false, error: "..." }`
44+
45+
#### Scenario: Underscores rejected
46+
- **WHEN** a change name like `add_auth` is validated
47+
- **THEN** validation returns `{ valid: false, error: "..." }`
48+
49+
#### Scenario: Special characters rejected
50+
- **WHEN** a change name like `add-auth!` is validated
51+
- **THEN** validation returns `{ valid: false, error: "..." }`
52+
53+
#### Scenario: Leading hyphen rejected
54+
- **WHEN** a change name like `-add-auth` is validated
55+
- **THEN** validation returns `{ valid: false, error: "..." }`
56+
57+
#### Scenario: Trailing hyphen rejected
58+
- **WHEN** a change name like `add-auth-` is validated
59+
- **THEN** validation returns `{ valid: false, error: "..." }`
60+
61+
#### Scenario: Consecutive hyphens rejected
62+
- **WHEN** a change name like `add--auth` is validated
63+
- **THEN** validation returns `{ valid: false, error: "..." }`
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## Phase 1: Implement Name Validation
2+
3+
- [x] 1.1 Create `src/utils/change-utils.ts`
4+
- [x] 1.2 Implement `validateChangeName()` with kebab-case pattern
5+
- [x] 1.3 Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`
6+
- [x] 1.4 Return `{ valid: boolean; error?: string }`
7+
- [x] 1.5 Add test: valid names accepted (`add-auth`, `refactor`, `add-feature-2`)
8+
- [x] 1.6 Add test: uppercase rejected
9+
- [x] 1.7 Add test: spaces rejected
10+
- [x] 1.8 Add test: underscores rejected
11+
- [x] 1.9 Add test: special characters rejected
12+
- [x] 1.10 Add test: leading/trailing hyphens rejected
13+
- [x] 1.11 Add test: consecutive hyphens rejected
14+
15+
## Phase 2: Implement Change Creation
16+
17+
- [x] 2.1 Implement `createChange(projectRoot, name)`
18+
- [x] 2.2 Validate name before creating
19+
- [x] 2.3 Create parent directories if needed (`openspec/changes/`)
20+
- [x] 2.4 Throw if change already exists
21+
- [x] 2.5 Add test: creates directory
22+
- [x] 2.6 Add test: duplicate change throws error
23+
- [x] 2.7 Add test: invalid name throws validation error
24+
- [x] 2.8 Add test: creates parent directories if needed
25+
26+
## Phase 3: Integration
27+
28+
- [x] 3.1 Export functions from `src/utils/index.ts`
29+
- [x] 3.2 Add JSDoc comments
30+
- [x] 3.3 Run all tests to verify no regressions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# change-creation Specification
2+
3+
## Purpose
4+
Provide programmatic utilities for creating and validating OpenSpec change directories.
5+
## Requirements
6+
### Requirement: Change Creation
7+
The system SHALL provide a function to create new change directories programmatically.
8+
9+
#### Scenario: Create change
10+
- **WHEN** `createChange(projectRoot, 'add-auth')` is called
11+
- **THEN** the system creates `openspec/changes/add-auth/` directory
12+
13+
#### Scenario: Duplicate change rejected
14+
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and `openspec/changes/add-auth/` already exists
15+
- **THEN** the system throws an error indicating the change already exists
16+
17+
#### Scenario: Creates parent directories if needed
18+
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and `openspec/changes/` does not exist
19+
- **THEN** the system creates the full path including parent directories
20+
21+
#### Scenario: Invalid change name rejected
22+
- **WHEN** `createChange(projectRoot, 'Add Auth')` is called with an invalid name
23+
- **THEN** the system throws a validation error
24+
25+
### Requirement: Change Name Validation
26+
The system SHALL validate change names follow kebab-case conventions.
27+
28+
#### Scenario: Valid kebab-case name accepted
29+
- **WHEN** a change name like `add-user-auth` is validated
30+
- **THEN** validation returns `{ valid: true }`
31+
32+
#### Scenario: Numeric suffixes accepted
33+
- **WHEN** a change name like `add-feature-2` is validated
34+
- **THEN** validation returns `{ valid: true }`
35+
36+
#### Scenario: Single word accepted
37+
- **WHEN** a change name like `refactor` is validated
38+
- **THEN** validation returns `{ valid: true }`
39+
40+
#### Scenario: Uppercase characters rejected
41+
- **WHEN** a change name like `Add-Auth` is validated
42+
- **THEN** validation returns `{ valid: false, error: "..." }`
43+
44+
#### Scenario: Spaces rejected
45+
- **WHEN** a change name like `add auth` is validated
46+
- **THEN** validation returns `{ valid: false, error: "..." }`
47+
48+
#### Scenario: Underscores rejected
49+
- **WHEN** a change name like `add_auth` is validated
50+
- **THEN** validation returns `{ valid: false, error: "..." }`
51+
52+
#### Scenario: Special characters rejected
53+
- **WHEN** a change name like `add-auth!` is validated
54+
- **THEN** validation returns `{ valid: false, error: "..." }`
55+
56+
#### Scenario: Leading hyphen rejected
57+
- **WHEN** a change name like `-add-auth` is validated
58+
- **THEN** validation returns `{ valid: false, error: "..." }`
59+
60+
#### Scenario: Trailing hyphen rejected
61+
- **WHEN** a change name like `add-auth-` is validated
62+
- **THEN** validation returns `{ valid: false, error: "..." }`
63+
64+
#### Scenario: Consecutive hyphens rejected
65+
- **WHEN** a change name like `add--auth` is validated
66+
- **THEN** validation returns `{ valid: false, error: "..." }`
67+

src/utils/change-utils.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import path from 'path';
2+
import { FileSystemUtils } from './file-system.js';
3+
4+
/**
5+
* Result of validating a change name.
6+
*/
7+
export interface ValidationResult {
8+
valid: boolean;
9+
error?: string;
10+
}
11+
12+
/**
13+
* Validates that a change name follows kebab-case conventions.
14+
*
15+
* Valid names:
16+
* - Start with a lowercase letter
17+
* - Contain only lowercase letters, numbers, and hyphens
18+
* - Do not start or end with a hyphen
19+
* - Do not contain consecutive hyphens
20+
*
21+
* @param name - The change name to validate
22+
* @returns Validation result with `valid: true` or `valid: false` with an error message
23+
*
24+
* @example
25+
* validateChangeName('add-auth') // { valid: true }
26+
* validateChangeName('Add-Auth') // { valid: false, error: '...' }
27+
*/
28+
export function validateChangeName(name: string): ValidationResult {
29+
// Pattern: starts with lowercase letter, followed by lowercase letters/numbers,
30+
// optionally followed by hyphen + lowercase letters/numbers (repeatable)
31+
const kebabCasePattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
32+
33+
if (!name) {
34+
return { valid: false, error: 'Change name cannot be empty' };
35+
}
36+
37+
if (!kebabCasePattern.test(name)) {
38+
// Provide specific error messages for common mistakes
39+
if (/[A-Z]/.test(name)) {
40+
return { valid: false, error: 'Change name must be lowercase (use kebab-case)' };
41+
}
42+
if (/\s/.test(name)) {
43+
return { valid: false, error: 'Change name cannot contain spaces (use hyphens instead)' };
44+
}
45+
if (/_/.test(name)) {
46+
return { valid: false, error: 'Change name cannot contain underscores (use hyphens instead)' };
47+
}
48+
if (name.startsWith('-')) {
49+
return { valid: false, error: 'Change name cannot start with a hyphen' };
50+
}
51+
if (name.endsWith('-')) {
52+
return { valid: false, error: 'Change name cannot end with a hyphen' };
53+
}
54+
if (/--/.test(name)) {
55+
return { valid: false, error: 'Change name cannot contain consecutive hyphens' };
56+
}
57+
if (/[^a-z0-9-]/.test(name)) {
58+
return { valid: false, error: 'Change name can only contain lowercase letters, numbers, and hyphens' };
59+
}
60+
if (/^[0-9]/.test(name)) {
61+
return { valid: false, error: 'Change name must start with a letter' };
62+
}
63+
64+
return { valid: false, error: 'Change name must follow kebab-case convention (e.g., add-auth, refactor-db)' };
65+
}
66+
67+
return { valid: true };
68+
}
69+
70+
/**
71+
* Creates a new change directory.
72+
*
73+
* @param projectRoot - The root directory of the project (where `openspec/` lives)
74+
* @param name - The change name (must be valid kebab-case)
75+
* @throws Error if the change name is invalid
76+
* @throws Error if the change directory already exists
77+
*
78+
* @example
79+
* // Creates openspec/changes/add-auth/
80+
* await createChange('/path/to/project', 'add-auth')
81+
*/
82+
export async function createChange(
83+
projectRoot: string,
84+
name: string
85+
): Promise<void> {
86+
// Validate the name first
87+
const validation = validateChangeName(name);
88+
if (!validation.valid) {
89+
throw new Error(validation.error);
90+
}
91+
92+
// Build the change directory path
93+
const changeDir = path.join(projectRoot, 'openspec', 'changes', name);
94+
95+
// Check if change already exists
96+
if (await FileSystemUtils.directoryExists(changeDir)) {
97+
throw new Error(`Change '${name}' already exists at ${changeDir}`);
98+
}
99+
100+
// Create the directory (including parent directories if needed)
101+
await FileSystemUtils.createDirectory(changeDir);
102+
}

src/utils/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
// Shared utilities will be implemented here
2-
export {};
1+
// Shared utilities
2+
export { validateChangeName, createChange } from './change-utils.js';
3+
export type { ValidationResult } from './change-utils.js';

0 commit comments

Comments
 (0)