Skip to content

Commit 4f47bd2

Browse files
committed
feat: unify change state model for scaffolded changes
- Update artifact workflow commands to work with scaffolded changes - Add draft changes section to dashboard view - Fix completed changes to require tasks.total > 0 - Archive unify-change-state-model change
1 parent 9eb2241 commit 4f47bd2

File tree

11 files changed

+682
-39
lines changed

11 files changed

+682
-39
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Design: Unify Change State Model
2+
3+
## Overview
4+
5+
This change fixes two bugs with minimal disruption to the existing system:
6+
7+
1. **View bug**: Empty changes incorrectly shown as "Completed"
8+
2. **Artifact workflow bug**: Commands fail on scaffolded changes
9+
10+
## Key Design Decision: Two Systems, Two Purposes
11+
12+
The task-based and artifact-based systems serve **different purposes** and should coexist:
13+
14+
| System | Purpose | Used By |
15+
|--------|---------|---------|
16+
| **Task Progress** | Track implementation work | `openspec view`, `openspec list` |
17+
| **Artifact Progress** | Track planning/spec work | `openspec status`, `openspec next` |
18+
19+
We do NOT merge these systems. Instead, we fix each to work correctly in its domain.
20+
21+
## Change 1: Fix View Command
22+
23+
### Current Logic (Buggy)
24+
25+
```typescript
26+
// view.ts line 90
27+
if (progress.total === 0 || progress.completed === progress.total) {
28+
completed.push({ name: entry.name });
29+
}
30+
```
31+
32+
Problem: `total === 0` means "no tasks defined yet", not "all tasks done".
33+
34+
### New Logic
35+
36+
```typescript
37+
if (progress.total === 0) {
38+
draft.push({ name: entry.name });
39+
} else if (progress.completed === progress.total) {
40+
completed.push({ name: entry.name });
41+
} else {
42+
active.push({ name: entry.name, progress });
43+
}
44+
```
45+
46+
### View Output Change
47+
48+
**Before:**
49+
```
50+
Completed Changes
51+
─────────────────
52+
✓ add-feature (all tasks done - correct)
53+
✓ test-workflow (no tasks - WRONG)
54+
```
55+
56+
**After:**
57+
```
58+
Draft Changes
59+
─────────────────
60+
○ test-workflow (no tasks yet)
61+
62+
Active Changes
63+
─────────────────
64+
◉ add-scaffold [████░░░░] 3/7 tasks
65+
66+
Completed Changes
67+
─────────────────
68+
✓ add-feature (all tasks done)
69+
```
70+
71+
## Change 2: Fix Artifact Workflow Discovery
72+
73+
### Current Logic (Buggy)
74+
75+
```typescript
76+
// artifact-workflow.ts - validateChangeExists()
77+
const activeChanges = await getActiveChangeIds(projectRoot);
78+
if (!activeChanges.includes(changeName)) {
79+
throw new Error(`Change '${changeName}' not found...`);
80+
}
81+
```
82+
83+
Problem: `getActiveChangeIds()` requires `proposal.md`, but artifact workflow should work on empty directories to help create the first artifact.
84+
85+
### New Logic
86+
87+
```typescript
88+
async function validateChangeExists(changeName: string, projectRoot: string): Promise<string> {
89+
const changePath = path.join(projectRoot, 'openspec', 'changes', changeName);
90+
91+
// Check directory existence directly, not proposal.md
92+
if (!fs.existsSync(changePath) || !fs.statSync(changePath).isDirectory()) {
93+
// List available changes for helpful error message
94+
const entries = await fs.promises.readdir(
95+
path.join(projectRoot, 'openspec', 'changes'),
96+
{ withFileTypes: true }
97+
);
98+
const available = entries
99+
.filter(e => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
100+
.map(e => e.name);
101+
102+
if (available.length === 0) {
103+
throw new Error('No changes found. Create one with: openspec new change <name>');
104+
}
105+
throw new Error(`Change '${changeName}' not found. Available:\n ${available.join('\n ')}`);
106+
}
107+
108+
return changeName;
109+
}
110+
```
111+
112+
### Behavior Change
113+
114+
```bash
115+
# Before
116+
$ openspec new change foo
117+
$ openspec status --change foo
118+
Error: Change 'foo' not found.
119+
120+
# After
121+
$ openspec new change foo
122+
$ openspec status --change foo
123+
Change: foo
124+
Progress: 0/4 artifacts complete
125+
126+
[ ] proposal
127+
[-] specs (blocked by: proposal)
128+
[-] design (blocked by: proposal)
129+
[-] tasks (blocked by: specs, design)
130+
```
131+
132+
## What Stays the Same
133+
134+
1. **`getActiveChangeIds()`** - Still requires `proposal.md` (used by validate, show)
135+
2. **`getArchivedChangeIds()`** - Unchanged
136+
3. **Active/Completed semantics** - Still based on task checkboxes
137+
4. **Validation** - Still requires `proposal.md` to have something to validate
138+
139+
## File Changes
140+
141+
| File | Change |
142+
|------|--------|
143+
| `src/core/view.ts` | Add draft category, fix completion logic |
144+
| `src/commands/artifact-workflow.ts` | Update `validateChangeExists()` to use directory existence |
145+
| `test/commands/artifact-workflow.test.ts` | Add tests for scaffolded changes |
146+
147+
## Testing Strategy
148+
149+
1. **Unit test**: `validateChangeExists()` with scaffolded change
150+
2. **View test**: Verify three categories render correctly
151+
3. **Manual test**: Full workflow from `new change``status``view`
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Proposal: Unify Change State Model
2+
3+
## Problem Statement
4+
5+
Two bugs create inconsistent behavior when working with changes:
6+
7+
### Bug 1: Empty changes shown as "Completed" in view
8+
9+
```typescript
10+
// view.ts line 90
11+
if (progress.total === 0 || progress.completed === progress.total) {
12+
completed.push({ name: entry.name }); // BUG: total === 0 ≠ completed
13+
}
14+
```
15+
16+
Result: `openspec new change foo && openspec view` shows `foo` as "Completed" when it has no content.
17+
18+
### Bug 2: Artifact workflow commands can't find scaffolded changes
19+
20+
```typescript
21+
// item-discovery.ts - getActiveChangeIds()
22+
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
23+
await fs.access(proposalPath); // Only returns changes WITH proposal.md
24+
```
25+
26+
Result: `openspec status --change foo` says "not found" even though the directory exists.
27+
28+
## Root Cause
29+
30+
The system conflates two different concepts:
31+
32+
| Concept | Question | Source of Truth |
33+
|---------|----------|-----------------|
34+
| **Planning Progress** | Are all spec documents created? | File existence (ArtifactGraph) |
35+
| **Implementation Progress** | Is the coding work done? | Task checkboxes (tasks.md) |
36+
37+
## Proposed Solution
38+
39+
### Fix 1: Add "Draft" state to view command
40+
41+
Keep Active/Completed with their existing meanings, but fix the bug:
42+
43+
| State | Criteria | Meaning |
44+
|-------|----------|---------|
45+
| **Draft** | No tasks.md OR `tasks.total === 0` | Still planning |
46+
| **Active** | `tasks.total > 0` AND `completed < total` | Implementing |
47+
| **Completed** | `tasks.total > 0` AND `completed === total` | Done |
48+
49+
### Fix 2: Artifact workflow uses directory existence
50+
51+
Update `validateChangeExists()` to check if the directory exists, not if `proposal.md` exists. This allows the artifact workflow to guide users through creating their first artifact.
52+
53+
### Keep existing discovery functions
54+
55+
`getActiveChangeIds()` continues to require `proposal.md` for backward compatibility with validation and other commands.
56+
57+
## What Changes
58+
59+
| Command | Before | After |
60+
|---------|--------|-------|
61+
| `openspec view` | Empty = "Completed" | Empty = "Draft" |
62+
| `openspec status --change X` | Requires proposal.md | Works on any directory |
63+
| `openspec validate X` | Requires proposal.md | Unchanged (still requires it) |
64+
65+
## Breaking Changes
66+
67+
### Minimal Breaking Change
68+
69+
1. **`openspec view` output**: Empty changes move from "Completed" section to new "Draft" section
70+
71+
### Non-Breaking
72+
73+
- Active/Completed semantics unchanged (still task-based)
74+
- `getActiveChangeIds()` unchanged
75+
- `openspec validate` unchanged
76+
- Archived changes unaffected
77+
78+
## Out of Scope
79+
80+
- Merging task-based and artifact-based progress (they serve different purposes)
81+
- Changing what "Completed" means (it stays = all tasks done)
82+
- Adding artifact progress to view command (separate enhancement)
83+
- Shell tab completions for artifact workflow commands (not yet registered)
84+
85+
## Related Commands Analysis
86+
87+
| Command | Uses `getActiveChangeIds()` | Should include scaffolded? | Change needed? |
88+
|---------|-----------------------------|-----------------------------|----------------|
89+
| `openspec view` | No (reads dirs directly) | Yes → Draft section | **Yes** |
90+
| `openspec list` | No (reads dirs directly) | Yes (shows "No tasks") | No |
91+
| `openspec status/next/instructions` | Yes | Yes | **Yes** |
92+
| `openspec validate` | Yes | No (can't validate empty) | No |
93+
| `openspec show` | Yes | No (nothing to show) | No |
94+
| Tab completions | Yes | Future enhancement | No |
95+
96+
## Success Criteria
97+
98+
1. `openspec new change foo && openspec view` shows `foo` in "Draft" section
99+
2. `openspec new change foo && openspec status --change foo` works
100+
3. Changes with all tasks done still show as "Completed"
101+
4. All existing tests pass
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# cli-artifact-workflow Specification Delta
2+
3+
## MODIFIED Requirements
4+
5+
### Requirement: Status Command
6+
7+
The system SHALL display artifact completion status for a change, including scaffolded (empty) changes.
8+
9+
> **Fixes bug**: Previously required `proposal.md` to exist via `getActiveChangeIds()`.
10+
11+
#### Scenario: Show status with all states
12+
13+
- **WHEN** user runs `openspec status --change <id>`
14+
- **THEN** the system displays each artifact with status indicator:
15+
- `[x]` for completed artifacts
16+
- `[ ]` for ready artifacts
17+
- `[-]` for blocked artifacts (with missing dependencies listed)
18+
19+
#### Scenario: Status shows completion summary
20+
21+
- **WHEN** user runs `openspec status --change <id>`
22+
- **THEN** output includes completion percentage and count (e.g., "2/4 artifacts complete")
23+
24+
#### Scenario: Status JSON output
25+
26+
- **WHEN** user runs `openspec status --change <id> --json`
27+
- **THEN** the system outputs JSON with changeName, schemaName, isComplete, and artifacts array
28+
29+
#### Scenario: Status on scaffolded change
30+
31+
- **WHEN** user runs `openspec status --change <id>` on a change with no artifacts
32+
- **THEN** system displays all artifacts with their status
33+
- **AND** root artifacts (no dependencies) show as ready `[ ]`
34+
- **AND** dependent artifacts show as blocked `[-]`
35+
36+
#### Scenario: Missing change parameter
37+
38+
- **WHEN** user runs `openspec status` without `--change`
39+
- **THEN** the system displays an error with list of available changes
40+
- **AND** includes scaffolded changes (directories without proposal.md)
41+
42+
#### Scenario: Unknown change
43+
44+
- **WHEN** user runs `openspec status --change unknown-id`
45+
- **AND** directory `openspec/changes/unknown-id/` does not exist
46+
- **THEN** the system displays an error listing all available change directories
47+
48+
### Requirement: Next Command
49+
50+
The system SHALL show which artifacts are ready to be created, including for scaffolded changes.
51+
52+
#### Scenario: Show ready artifacts
53+
54+
- **WHEN** user runs `openspec next --change <id>`
55+
- **THEN** the system lists artifacts whose dependencies are all satisfied
56+
57+
#### Scenario: No artifacts ready
58+
59+
- **WHEN** all artifacts are either completed or blocked
60+
- **THEN** the system indicates no artifacts are ready (with explanation)
61+
62+
#### Scenario: All artifacts complete
63+
64+
- **WHEN** all artifacts in the change are completed
65+
- **THEN** the system indicates the change is complete
66+
67+
#### Scenario: Next JSON output
68+
69+
- **WHEN** user runs `openspec next --change <id> --json`
70+
- **THEN** the system outputs JSON array of ready artifact IDs
71+
72+
#### Scenario: Next on scaffolded change
73+
74+
- **WHEN** user runs `openspec next --change <id>` on a change with no artifacts
75+
- **THEN** system shows root artifacts (e.g., "proposal") as ready to create
76+
77+
### Requirement: Instructions Command
78+
79+
The system SHALL output enriched instructions for creating an artifact, including for scaffolded changes.
80+
81+
#### Scenario: Show enriched instructions
82+
83+
- **WHEN** user runs `openspec instructions <artifact> --change <id>`
84+
- **THEN** the system outputs:
85+
- Artifact metadata (ID, output path, description)
86+
- Template content
87+
- Dependency status (done/missing)
88+
- Unlocked artifacts (what becomes available after completion)
89+
90+
#### Scenario: Instructions JSON output
91+
92+
- **WHEN** user runs `openspec instructions <artifact> --change <id> --json`
93+
- **THEN** the system outputs JSON matching ArtifactInstructions interface
94+
95+
#### Scenario: Unknown artifact
96+
97+
- **WHEN** user runs `openspec instructions unknown-artifact --change <id>`
98+
- **THEN** the system displays an error listing valid artifact IDs for the schema
99+
100+
#### Scenario: Artifact with unmet dependencies
101+
102+
- **WHEN** user requests instructions for a blocked artifact
103+
- **THEN** the system displays instructions with a warning about missing dependencies
104+
105+
#### Scenario: Instructions on scaffolded change
106+
107+
- **WHEN** user runs `openspec instructions proposal --change <id>` on a scaffolded change
108+
- **THEN** system outputs template and metadata for creating the proposal
109+
- **AND** does not require any artifacts to already exist

0 commit comments

Comments
 (0)