Skip to content
337 changes: 200 additions & 137 deletions docs/artifact_poc.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
## Context

This is Slice 2 of the artifact tracker POC. The goal is to provide utilities for creating change directories programmatically.

**Current state:** No programmatic way to create changes. Users must manually create directories.

**Proposed state:** Utility functions for change creation with name validation.

## Goals / Non-Goals

### Goals
- **Add** `createChange()` function to create change directories
- **Add** `validateChangeName()` function for kebab-case validation
- **Enable** automation (Claude commands, scripts) to create changes

### Non-Goals
- Refactor existing CLI commands (they work fine)
- Create abstraction layers or manager classes
- Change how `ListCommand` or `ChangeCommand` work

## Decisions

### Decision 1: Simple Utility Functions

**Choice**: Add functions to `src/utils/change-utils.ts` - no class.

```typescript
// src/utils/change-utils.ts

export function validateChangeName(name: string): { valid: boolean; error?: string }

export async function createChange(
projectRoot: string,
name: string
): Promise<void>
```

**Why**:
- Simple, no abstraction overhead
- Easy to test
- Easy to import where needed
- Matches existing utility patterns in `src/utils/`

**Alternatives considered**:
- ChangeManager class: Rejected - over-engineered for 2 functions
- Add to existing command: Rejected - mixes CLI with reusable logic

### Decision 2: Kebab-Case Validation Pattern

**Choice**: Validate names with `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`

Valid: `add-auth`, `refactor-db`, `add-feature-2`, `refactor`
Invalid: `Add-Auth`, `add auth`, `add_auth`, `-add-auth`, `add-auth-`, `add--auth`

**Why**:
- Filesystem-safe (no special characters)
- URL-safe (for future web UI)
- Consistent with existing change naming in repo

## File Changes

### New Files
- `src/utils/change-utils.ts` - Utility functions
- `src/utils/change-utils.test.ts` - Unit tests

### Modified Files
- None

## Risks / Trade-offs

| Risk | Mitigation |
|------|------------|
| Function might not cover all use cases | Start simple, extend if needed |
| Naming conflicts with future work | Using clear, specific function names |
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Why

There's no programmatic way to create a new change directory. Users must manually:
1. Create `openspec/changes/<name>/` directory
2. Create a `proposal.md` file
3. Hope they got the naming right

This is error-prone and blocks automation (e.g., Claude commands, scripts).

**This proposal adds:**
1. `createChange(projectRoot, name)` - Create change directories programmatically
2. `validateChangeName(name)` - Enforce kebab-case naming conventions

## What Changes

### New Utilities

| Function | Description |
|----------|-------------|
| `createChange(projectRoot, name)` | Creates `openspec/changes/<name>/` directory |
| `validateChangeName(name)` | Returns `{ valid: boolean; error?: string }` |

### Name Validation Rules

Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`

| Valid | Invalid |
|-------|---------|
| `add-auth` | `Add-Auth` (uppercase) |
| `refactor-db` | `add auth` (spaces) |
| `add-feature-2` | `add_auth` (underscores) |
| `refactor` | `-add-auth` (leading hyphen) |

### Location

New file: `src/utils/change-utils.ts`

Simple utility functions - no class, no abstraction layer.

## Impact

- **Affected specs**: None
- **Affected code**: None (new utilities only)
- **New files**: `src/utils/change-utils.ts`
- **Breaking changes**: None
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## ADDED Requirements

### Requirement: Change Creation
The system SHALL provide a function to create new change directories programmatically.

#### Scenario: Create change
- **WHEN** `createChange(projectRoot, 'add-auth')` is called
- **THEN** the system creates `openspec/changes/add-auth/` directory

#### Scenario: Duplicate change rejected
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and `openspec/changes/add-auth/` already exists
- **THEN** the system throws an error indicating the change already exists

#### Scenario: Creates parent directories if needed
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and `openspec/changes/` does not exist
- **THEN** the system creates the full path including parent directories

#### Scenario: Invalid change name rejected
- **WHEN** `createChange(projectRoot, 'Add Auth')` is called with an invalid name
- **THEN** the system throws a validation error

### Requirement: Change Name Validation
The system SHALL validate change names follow kebab-case conventions.

#### Scenario: Valid kebab-case name accepted
- **WHEN** a change name like `add-user-auth` is validated
- **THEN** validation returns `{ valid: true }`

#### Scenario: Numeric suffixes accepted
- **WHEN** a change name like `add-feature-2` is validated
- **THEN** validation returns `{ valid: true }`

#### Scenario: Single word accepted
- **WHEN** a change name like `refactor` is validated
- **THEN** validation returns `{ valid: true }`

#### Scenario: Uppercase characters rejected
- **WHEN** a change name like `Add-Auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Spaces rejected
- **WHEN** a change name like `add auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Underscores rejected
- **WHEN** a change name like `add_auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Special characters rejected
- **WHEN** a change name like `add-auth!` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Leading hyphen rejected
- **WHEN** a change name like `-add-auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Trailing hyphen rejected
- **WHEN** a change name like `add-auth-` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Consecutive hyphens rejected
- **WHEN** a change name like `add--auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`
30 changes: 30 additions & 0 deletions openspec/changes/archive/2025-12-25-add-change-manager/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## Phase 1: Implement Name Validation

- [x] 1.1 Create `src/utils/change-utils.ts`
- [x] 1.2 Implement `validateChangeName()` with kebab-case pattern
- [x] 1.3 Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`
- [x] 1.4 Return `{ valid: boolean; error?: string }`
- [x] 1.5 Add test: valid names accepted (`add-auth`, `refactor`, `add-feature-2`)
- [x] 1.6 Add test: uppercase rejected
- [x] 1.7 Add test: spaces rejected
- [x] 1.8 Add test: underscores rejected
- [x] 1.9 Add test: special characters rejected
- [x] 1.10 Add test: leading/trailing hyphens rejected
- [x] 1.11 Add test: consecutive hyphens rejected

## Phase 2: Implement Change Creation

- [x] 2.1 Implement `createChange(projectRoot, name)`
- [x] 2.2 Validate name before creating
- [x] 2.3 Create parent directories if needed (`openspec/changes/`)
- [x] 2.4 Throw if change already exists
- [x] 2.5 Add test: creates directory
- [x] 2.6 Add test: duplicate change throws error
- [x] 2.7 Add test: invalid name throws validation error
- [x] 2.8 Add test: creates parent directories if needed

## Phase 3: Integration

- [x] 3.1 Export functions from `src/utils/index.ts`
- [x] 3.2 Add JSDoc comments
- [x] 3.3 Run all tests to verify no regressions
67 changes: 67 additions & 0 deletions openspec/specs/change-creation/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# change-creation Specification

## Purpose
Provide programmatic utilities for creating and validating OpenSpec change directories.
## Requirements
### Requirement: Change Creation
The system SHALL provide a function to create new change directories programmatically.

#### Scenario: Create change
- **WHEN** `createChange(projectRoot, 'add-auth')` is called
- **THEN** the system creates `openspec/changes/add-auth/` directory

#### Scenario: Duplicate change rejected
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and `openspec/changes/add-auth/` already exists
- **THEN** the system throws an error indicating the change already exists

#### Scenario: Creates parent directories if needed
- **WHEN** `createChange(projectRoot, 'add-auth')` is called and `openspec/changes/` does not exist
- **THEN** the system creates the full path including parent directories

#### Scenario: Invalid change name rejected
- **WHEN** `createChange(projectRoot, 'Add Auth')` is called with an invalid name
- **THEN** the system throws a validation error

### Requirement: Change Name Validation
The system SHALL validate change names follow kebab-case conventions.

#### Scenario: Valid kebab-case name accepted
- **WHEN** a change name like `add-user-auth` is validated
- **THEN** validation returns `{ valid: true }`

#### Scenario: Numeric suffixes accepted
- **WHEN** a change name like `add-feature-2` is validated
- **THEN** validation returns `{ valid: true }`

#### Scenario: Single word accepted
- **WHEN** a change name like `refactor` is validated
- **THEN** validation returns `{ valid: true }`

#### Scenario: Uppercase characters rejected
- **WHEN** a change name like `Add-Auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Spaces rejected
- **WHEN** a change name like `add auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Underscores rejected
- **WHEN** a change name like `add_auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Special characters rejected
- **WHEN** a change name like `add-auth!` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Leading hyphen rejected
- **WHEN** a change name like `-add-auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Trailing hyphen rejected
- **WHEN** a change name like `add-auth-` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

#### Scenario: Consecutive hyphens rejected
- **WHEN** a change name like `add--auth` is validated
- **THEN** validation returns `{ valid: false, error: "..." }`

102 changes: 102 additions & 0 deletions src/utils/change-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import path from 'path';
import { FileSystemUtils } from './file-system.js';

/**
* Result of validating a change name.
*/
export interface ValidationResult {
valid: boolean;
error?: string;
}

/**
* Validates that a change name follows kebab-case conventions.
*
* Valid names:
* - Start with a lowercase letter
* - Contain only lowercase letters, numbers, and hyphens
* - Do not start or end with a hyphen
* - Do not contain consecutive hyphens
*
* @param name - The change name to validate
* @returns Validation result with `valid: true` or `valid: false` with an error message
*
* @example
* validateChangeName('add-auth') // { valid: true }
* validateChangeName('Add-Auth') // { valid: false, error: '...' }
*/
export function validateChangeName(name: string): ValidationResult {
// Pattern: starts with lowercase letter, followed by lowercase letters/numbers,
// optionally followed by hyphen + lowercase letters/numbers (repeatable)
const kebabCasePattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;

if (!name) {
return { valid: false, error: 'Change name cannot be empty' };
}

if (!kebabCasePattern.test(name)) {
// Provide specific error messages for common mistakes
if (/[A-Z]/.test(name)) {
return { valid: false, error: 'Change name must be lowercase (use kebab-case)' };
}
if (/\s/.test(name)) {
return { valid: false, error: 'Change name cannot contain spaces (use hyphens instead)' };
}
if (/_/.test(name)) {
return { valid: false, error: 'Change name cannot contain underscores (use hyphens instead)' };
}
if (name.startsWith('-')) {
return { valid: false, error: 'Change name cannot start with a hyphen' };
}
if (name.endsWith('-')) {
return { valid: false, error: 'Change name cannot end with a hyphen' };
}
if (/--/.test(name)) {
return { valid: false, error: 'Change name cannot contain consecutive hyphens' };
}
if (/[^a-z0-9-]/.test(name)) {
return { valid: false, error: 'Change name can only contain lowercase letters, numbers, and hyphens' };
}
if (/^[0-9]/.test(name)) {
return { valid: false, error: 'Change name must start with a letter' };
}

return { valid: false, error: 'Change name must follow kebab-case convention (e.g., add-auth, refactor-db)' };
}

return { valid: true };
}

/**
* Creates a new change directory.
*
* @param projectRoot - The root directory of the project (where `openspec/` lives)
* @param name - The change name (must be valid kebab-case)
* @throws Error if the change name is invalid
* @throws Error if the change directory already exists
*
* @example
* // Creates openspec/changes/add-auth/
* await createChange('/path/to/project', 'add-auth')
*/
export async function createChange(
projectRoot: string,
name: string
): Promise<void> {
// Validate the name first
const validation = validateChangeName(name);
if (!validation.valid) {
throw new Error(validation.error);
}

// Build the change directory path
const changeDir = path.join(projectRoot, 'openspec', 'changes', name);

// Check if change already exists
if (await FileSystemUtils.directoryExists(changeDir)) {
throw new Error(`Change '${name}' already exists at ${changeDir}`);
}

// Create the directory (including parent directories if needed)
await FileSystemUtils.createDirectory(changeDir);
}
5 changes: 3 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// Shared utilities will be implemented here
export {};
// Shared utilities
export { validateChangeName, createChange } from './change-utils.js';
export type { ValidationResult } from './change-utils.js';
Loading
Loading