Skip to content

Commit a835b4f

Browse files
committed
experiment: add vertical slice version of artifact graph change
Creates add-artifact-graph-core-v2 with requirements organized as vertical slices - each requirement file contains its spec, design decisions, and tasks bundled together for comparison.
1 parent 5f8cb32 commit a835b4f

File tree

6 files changed

+420
-0
lines changed

6 files changed

+420
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Overview
2+
3+
## Context
4+
5+
This implements "Slice 1: What's Ready?" from the artifact POC analysis. The core insight is using the filesystem as a database - artifact completion is detected by file existence, making the system stateless and version-control friendly.
6+
7+
This module will coexist with the current OpenSpec system as a parallel capability, potentially enabling future migration or integration.
8+
9+
## Goals / Non-Goals
10+
11+
**Goals:**
12+
- Pure dependency graph logic with no side effects
13+
- Stateless state detection (rescan filesystem each query)
14+
- Support glob patterns for multi-file artifacts (e.g., `specs/*.md`)
15+
- Load artifact definitions from YAML schemas
16+
- Calculate topological build order
17+
- Determine "ready" artifacts based on dependency completion
18+
19+
**Non-Goals:**
20+
- CLI commands (Slice 4)
21+
- Multi-change management (Slice 2)
22+
- Template resolution and enrichment (Slice 3)
23+
- Agent integration or Claude commands
24+
- Replacing existing OpenSpec functionality
25+
26+
## Data Structures
27+
28+
**Zod Schemas (source of truth):**
29+
30+
```typescript
31+
import { z } from 'zod';
32+
33+
// Artifact definition schema
34+
export const ArtifactSchema = z.object({
35+
id: z.string().min(1, 'Artifact ID is required'),
36+
generates: z.string().min(1), // e.g., "proposal.md" or "specs/*.md"
37+
description: z.string(),
38+
template: z.string(), // path to template file
39+
requires: z.array(z.string()).default([]),
40+
});
41+
42+
// Full schema YAML structure
43+
export const SchemaYamlSchema = z.object({
44+
name: z.string().min(1, 'Schema name is required'),
45+
version: z.number().int().positive(),
46+
description: z.string().optional(),
47+
artifacts: z.array(ArtifactSchema).min(1, 'At least one artifact required'),
48+
});
49+
50+
// Derived TypeScript types
51+
export type Artifact = z.infer<typeof ArtifactSchema>;
52+
export type SchemaYaml = z.infer<typeof SchemaYamlSchema>;
53+
```
54+
55+
**Runtime State (not Zod - internal only):**
56+
57+
```typescript
58+
// Slice 1: Simple completion tracking via filesystem
59+
type CompletedSet = Set<string>;
60+
61+
// Return type for blocked query
62+
interface BlockedArtifacts {
63+
[artifactId: string]: string[]; // artifact → list of unmet dependencies
64+
}
65+
66+
interface ArtifactGraphResult {
67+
completed: string[];
68+
ready: string[];
69+
blocked: BlockedArtifacts;
70+
buildOrder: string[];
71+
}
72+
```
73+
74+
## File Structure
75+
76+
```
77+
src/core/artifact-graph/
78+
├── index.ts # Public exports
79+
├── types.ts # Zod schemas and type definitions
80+
├── graph.ts # ArtifactGraph class
81+
├── state.ts # State detection logic
82+
├── resolver.ts # Schema resolution (global → built-in)
83+
└── schemas/ # Built-in schema definitions (package level)
84+
├── spec-driven.yaml # Default: proposal → specs → design → tasks
85+
└── tdd.yaml # Alternative: tests → implementation → docs
86+
```
87+
88+
**Schema Resolution Paths:**
89+
- Global user override: `~/.config/openspec/schemas/<name>.yaml`
90+
- Package built-in: `src/core/artifact-graph/schemas/<name>.yaml` (bundled with package)
91+
92+
## Risks / Trade-offs
93+
94+
| Risk | Mitigation |
95+
|------|------------|
96+
| Glob pattern edge cases | Use well-tested glob library (fast-glob or similar) |
97+
| Cycle detection | Kahn's algorithm naturally fails on cycles; provide clear error |
98+
| Schema evolution | Version field in schema, validate on load |
99+
100+
## Cross-Cutting Tasks
101+
102+
- [ ] 8.1 Create `src/core/artifact-graph/index.ts` with public exports
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## Why
2+
3+
The current OpenSpec system relies on conventions and AI inference for artifact ordering. A formal artifact graph with dependency awareness would enable deterministic "what's ready?" queries, making the system more predictable and enabling future features like automated pipeline execution.
4+
5+
## What Changes
6+
7+
- Add `ArtifactGraph` class to model artifacts as a DAG with dependency relationships
8+
- Add `ArtifactState` type to track completion status (completed, in_progress, failed)
9+
- Add filesystem-based state detection using file existence and glob patterns
10+
- Add schema YAML parser to load artifact definitions
11+
- Implement topological sort (Kahn's algorithm) for build order calculation
12+
- Add `getNextArtifacts()` to find artifacts ready for creation
13+
14+
## Impact
15+
16+
- Affected specs: New `artifact-graph` capability
17+
- Affected code: `src/core/artifact-graph/` (new directory)
18+
- No changes to existing functionality - this is a parallel module
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Schema Loading
2+
3+
## Requirement
4+
5+
The system SHALL load artifact graph definitions from YAML schema files.
6+
7+
## Scenarios
8+
9+
### Scenario: Valid schema loaded
10+
- **WHEN** a valid schema YAML file is provided
11+
- **THEN** the system returns an ArtifactGraph with all artifacts and dependencies
12+
13+
### Scenario: Invalid schema rejected
14+
- **WHEN** a schema YAML file is missing required fields
15+
- **THEN** the system throws an error with a descriptive message
16+
17+
### Scenario: Cyclic dependencies detected
18+
- **WHEN** a schema contains cyclic artifact dependencies
19+
- **THEN** the system throws an error listing the artifact IDs in the cycle
20+
21+
### Scenario: Invalid dependency reference
22+
- **WHEN** an artifact's `requires` array references a non-existent artifact ID
23+
- **THEN** the system throws an error identifying the invalid reference
24+
25+
### Scenario: Duplicate artifact IDs rejected
26+
- **WHEN** a schema contains multiple artifacts with the same ID
27+
- **THEN** the system throws an error identifying the duplicate
28+
29+
## Design
30+
31+
### Decision: Zod for Schema Validation
32+
Use Zod for validating YAML schema structure and deriving TypeScript types.
33+
34+
**Rationale:**
35+
- Already a project dependency (v4.0.17) used in `src/core/schemas/`
36+
- Type inference via `z.infer<>` - single source of truth for types
37+
- Runtime validation with detailed error messages
38+
- Consistent with existing project patterns (`base.schema.ts`, `config-schema.ts`)
39+
40+
**Alternatives considered:**
41+
- Manual validation: More code, error-prone, no type inference
42+
- JSON Schema: Would require additional dependency, less TypeScript integration
43+
- io-ts: Not already in project, steeper learning curve
44+
45+
### Decision: Two-Level Schema Resolution
46+
Schemas resolve from global user config, falling back to package built-ins.
47+
48+
**Resolution order:**
49+
1. `~/.config/openspec/schemas/<name>.yaml` - Global user override
50+
2. `<package>/schemas/<name>.yaml` - Built-in defaults
51+
52+
**Rationale:**
53+
- Follows common CLI patterns (ESLint, Prettier, Git, npm)
54+
- Built-ins baked into package, never auto-copied
55+
- Users customize by creating files in global config dir
56+
- Simple - no project-level overrides (can add later if needed)
57+
58+
**Alternatives considered:**
59+
- Project-level overrides: Added complexity, not needed initially
60+
- Auto-copy to user space: Creates drift, harder to update defaults
61+
62+
### Decision: Cycle Error Format
63+
Cycle errors list all artifact IDs in the cycle for easy debugging.
64+
65+
**Format:** `"Cyclic dependency detected: A → B → C → A"`
66+
67+
**Rationale:**
68+
- Shows the full cycle path, not just that a cycle exists
69+
- Actionable - developer can see exactly which artifacts to fix
70+
- Consistent with Kahn's algorithm which naturally identifies cycle participants
71+
72+
### Decision: Template Field Parsed But Not Resolved
73+
The `template` field is required in schema YAML for completeness, but template resolution is deferred to Slice 3.
74+
75+
**Rationale:**
76+
- Slice 1 focuses on "What's Ready?" - dependency and completion queries only
77+
- Template paths are validated syntactically (non-empty string) but not resolved
78+
- Keeps Slice 1 focused and independently testable
79+
80+
## Tasks
81+
82+
### Type Definitions
83+
- [ ] 1.1 Create `src/core/artifact-graph/types.ts` with Zod schemas (`ArtifactSchema`, `SchemaYamlSchema`) and inferred types via `z.infer<>`
84+
- [ ] 1.2 Define `CompletedSet` (Set<string>), `BlockedArtifacts`, and `ArtifactGraphResult` types for runtime state
85+
86+
### Schema Parser
87+
- [ ] 2.1 Create `src/core/artifact-graph/schema.ts` with YAML loading and Zod validation via `.safeParse()`
88+
- [ ] 2.2 Implement dependency reference validation (ensure `requires` references valid artifact IDs)
89+
- [ ] 2.3 Implement duplicate artifact ID detection
90+
- [ ] 2.4 Add cycle detection during schema load (error format: "Cyclic dependency detected: A → B → C → A")
91+
92+
### Schema Resolution
93+
- [ ] 6.1 Create `src/core/artifact-graph/resolver.ts` with schema resolution logic
94+
- [ ] 6.2 Implement `resolveSchema(name)` - global (`~/.config/openspec/schemas/`) → built-in fallback
95+
- [ ] 6.3 Use existing `getGlobalConfigDir()` from `src/core/global-config.ts`
96+
97+
### Built-in Schemas
98+
- [ ] 7.1 Create `src/core/artifact-graph/schemas/spec-driven.yaml` (default: proposal → specs → design → tasks)
99+
- [ ] 7.2 Create `src/core/artifact-graph/schemas/tdd.yaml` (alternative: tests → implementation → docs)
100+
101+
### Tests
102+
- [ ] 9.1 Test: Parse valid schema YAML returns correct artifact graph
103+
- [ ] 9.2 Test: Parse invalid schema (missing fields) throws descriptive error
104+
- [ ] 9.3 Test: Duplicate artifact IDs throws error
105+
- [ ] 9.4 Test: Invalid `requires` reference throws error identifying the invalid ID
106+
- [ ] 9.5 Test: Cycle in schema throws error listing cycle path (e.g., "A → B → C → A")
107+
- [ ] 9.18 Test: Schema resolution finds global override before built-in
108+
- [ ] 9.19 Test: Schema resolution falls back to built-in when no global
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Build Order Calculation
2+
3+
## Requirement
4+
5+
The system SHALL compute a valid topological build order for artifacts.
6+
7+
## Scenarios
8+
9+
### Scenario: Linear dependency chain
10+
- **WHEN** artifacts form a linear chain (A → B → C)
11+
- **THEN** getBuildOrder() returns [A, B, C]
12+
13+
### Scenario: Diamond dependency
14+
- **WHEN** artifacts form a diamond (A → B, A → C, B → D, C → D)
15+
- **THEN** getBuildOrder() returns A before B and C, and D last
16+
17+
### Scenario: Independent artifacts
18+
- **WHEN** artifacts have no dependencies
19+
- **THEN** getBuildOrder() returns them in a stable order
20+
21+
## Design
22+
23+
### Decision: Kahn's Algorithm for Topological Sort
24+
Use Kahn's algorithm for computing build order.
25+
26+
**Rationale:**
27+
- Well-understood, O(V+E) complexity
28+
- Naturally detects cycles during execution
29+
- Produces a stable, deterministic order
30+
31+
## Tasks
32+
33+
### Artifact Graph Core
34+
- [ ] 3.1 Create `src/core/artifact-graph/graph.ts` with ArtifactGraph class
35+
- [ ] 3.2 Implement `fromYaml(path)` - load graph from schema file
36+
- [ ] 3.3 Implement `getBuildOrder()` - topological sort via Kahn's algorithm
37+
- [ ] 3.4 Implement `getArtifact(id)` - retrieve single artifact definition
38+
- [ ] 3.5 Implement `getAllArtifacts()` - list all artifacts
39+
40+
### Tests
41+
- [ ] 9.6 Test: Compute build order returns correct topological ordering (linear chain)
42+
- [ ] 9.7 Test: Compute build order handles diamond dependencies correctly
43+
- [ ] 9.8 Test: Independent artifacts return in stable order
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# State Detection
2+
3+
## Requirement
4+
5+
The system SHALL detect artifact completion state by scanning the filesystem.
6+
7+
## Scenarios
8+
9+
### Scenario: Simple file exists
10+
- **WHEN** an artifact generates "proposal.md" and the file exists
11+
- **THEN** the artifact is marked as completed
12+
13+
### Scenario: Simple file missing
14+
- **WHEN** an artifact generates "proposal.md" and the file does not exist
15+
- **THEN** the artifact is not marked as completed
16+
17+
### Scenario: Glob pattern with files
18+
- **WHEN** an artifact generates "specs/*.md" and the specs/ directory contains .md files
19+
- **THEN** the artifact is marked as completed
20+
21+
### Scenario: Glob pattern empty
22+
- **WHEN** an artifact generates "specs/*.md" and the specs/ directory is empty or missing
23+
- **THEN** the artifact is not marked as completed
24+
25+
### Scenario: Missing change directory
26+
- **WHEN** the change directory does not exist
27+
- **THEN** all artifacts are marked as not completed (empty state)
28+
29+
## Design
30+
31+
### Decision: Filesystem as Database
32+
Use file existence for state detection rather than a separate state file.
33+
34+
**Rationale:**
35+
- Stateless - no state corruption possible
36+
- Git-friendly - state derived from committed files
37+
- Simple - no sync issues between state file and actual files
38+
39+
**Alternatives considered:**
40+
- JSON/SQLite state file: More complex, sync issues, not git-friendly
41+
- Git metadata: Too coupled to git, complex implementation
42+
43+
### Decision: Glob Pattern Support
44+
Support glob patterns like `specs/*.md` in artifact `generates` field.
45+
46+
**Rationale:**
47+
- Allows multiple files to satisfy a single artifact requirement
48+
- Common pattern for spec directories with multiple files
49+
- Uses standard glob syntax
50+
51+
### Decision: Immutable Completed Set
52+
Represent completion state as an immutable Set of completed artifact IDs.
53+
54+
**Rationale:**
55+
- Functional style, easier to reason about
56+
- State derived fresh each query, no mutation needed
57+
- Clear separation between graph structure and runtime state
58+
- Filesystem can only detect binary existence (complete vs not complete)
59+
60+
**Note:** `inProgress` and `failed` states are deferred to future slices. They would require external state tracking (e.g., a status file) since file existence alone cannot distinguish these states.
61+
62+
## Tasks
63+
64+
### State Detection
65+
- [ ] 4.1 Create `src/core/artifact-graph/state.ts` with state detection logic
66+
- [ ] 4.2 Implement file existence checking for simple paths
67+
- [ ] 4.3 Implement glob pattern matching for multi-file artifacts
68+
- [ ] 4.4 Implement `detectCompleted(graph, changeDir)` - scan filesystem and return CompletedSet
69+
- [ ] 4.5 Handle missing changeDir gracefully (return empty CompletedSet)
70+
71+
### Tests
72+
- [ ] 9.9 Test: Empty/missing changeDir returns empty CompletedSet
73+
- [ ] 9.10 Test: File existence marks artifact as completed
74+
- [ ] 9.11 Test: Glob pattern specs/*.md detected as complete when files exist
75+
- [ ] 9.12 Test: Glob pattern with empty directory not marked complete

0 commit comments

Comments
 (0)