Skip to content

Commit af273b8

Browse files
authored
proposal: add artifact graph core query system (#400)
* proposal: add artifact graph core query system Add OpenSpec change proposal for Slice 1 of the artifact POC - the core "What's Ready?" query system. This implements: - ArtifactGraph class for DAG-based dependency modeling - Filesystem-based state detection (file existence = completion) - Topological sort for build order calculation - Ready/blocked artifact queries This is a parallel module that will coexist with the current system. * docs: specify Zod for schema validation in artifact graph proposal - Add decision section for Zod schema validation in design.md - Update data structures to show Zod schemas with z.infer<> types - Update tasks to specify Zod usage for type definitions and parsing * docs: add 2-level schema resolution and built-in schemas - Add decision for global → built-in schema resolution pattern - Add resolver.ts for schema lookup logic - Add built-in schemas directory (spec-driven.yaml, tdd.yaml) - Add schema resolution tests - Follows ESLint/Prettier/Git patterns (defaults baked in package) * 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. * feat(core): add getGlobalDataDir for XDG-compliant data directory Add getGlobalDataDir() function following XDG Base Directory Specification for storing user data like schema overrides: - XDG_DATA_HOME takes precedence on all platforms - Unix/macOS fallback: ~/.local/share/openspec/ - Windows fallback: %LOCALAPPDATA%/openspec/ * feat(artifact-graph): add core dependency graph module Implement Slice 1 ("What's Ready?") of the artifact graph system: - types.ts: Zod schemas for artifact definitions with derived TypeScript types - schema.ts: YAML parsing with validation for duplicates, invalid refs, cycles - graph.ts: ArtifactGraph class with Kahn's algorithm for topological sort - state.ts: Filesystem-based completion detection with glob pattern support - resolver.ts: Two-level schema resolution (global override → built-in) - builtin-schemas.ts: spec-driven and tdd workflow definitions Key design decisions: - Filesystem as database (stateless, git-friendly) - Cycle errors show full path (e.g., "A → B → C → A") - Deterministic ordering via sorted queues * test(artifact-graph): add comprehensive test suite 52 tests covering all artifact-graph functionality: - schema.test.ts: Parsing, validation errors, cycle detection - graph.test.ts: Build order, ready artifacts, blocked queries - state.test.ts: File existence, glob patterns, missing directories - resolver.test.ts: Schema resolution with global overrides * docs(openspec): archive add-artifact-graph-core change Archive completed change proposal and create artifact-graph spec with 6 requirements covering schema loading, build order, state detection, ready queries, completion checks, and blocked queries. * chore: remove experimental artifact-graph-core-v2 folder Clean up experimental vertical slice proposal that is no longer needed. * feat(artifact-graph): validate global schema overrides Global schema overrides are now validated through the same pipeline as built-in schemas, catching invalid schemas, cyclic dependencies, and invalid requires references at load time. Added SchemaLoadError for better error context with file paths. * test(artifact-graph): add workflow integration tests Add end-to-end integration tests that exercise the full artifact-graph pipeline: resolveSchema → ArtifactGraph → detectCompleted → queries. Tests cover: - Complete spec-driven and tdd workflow progressions - Out-of-order file creation handling - Glob pattern matching with multiple files - Build order consistency - Edge cases (empty/missing directories, non-matching files) * refactor(artifact-graph): adopt zod v4 error message format Update custom error messages from string format to zod v4 object format using `{ error: 'message' }` convention. * fix(test): prevent hanging vitest threads after test runs - Add teardownTimeout (3s) to vitest config for forced cleanup - Add global teardown function to vitest.setup.ts - Call child.unref() to prevent child processes from blocking event loop - Explicitly destroy stdio streams on process close/error
1 parent 3ceef2d commit af273b8

File tree

24 files changed

+2323
-13
lines changed

24 files changed

+2323
-13
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
## Context
2+
3+
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.
4+
5+
This module will coexist with the current OpenSpec system as a parallel capability, potentially enabling future migration or integration.
6+
7+
## Goals / Non-Goals
8+
9+
**Goals:**
10+
- Pure dependency graph logic with no side effects
11+
- Stateless state detection (rescan filesystem each query)
12+
- Support glob patterns for multi-file artifacts (e.g., `specs/*.md`)
13+
- Load artifact definitions from YAML schemas
14+
- Calculate topological build order
15+
- Determine "ready" artifacts based on dependency completion
16+
17+
**Non-Goals:**
18+
- CLI commands (Slice 4)
19+
- Multi-change management (Slice 2)
20+
- Template resolution and enrichment (Slice 3)
21+
- Agent integration or Claude commands
22+
- Replacing existing OpenSpec functionality
23+
24+
## Decisions
25+
26+
### Decision: Filesystem as Database
27+
Use file existence for state detection rather than a separate state file.
28+
29+
**Rationale:**
30+
- Stateless - no state corruption possible
31+
- Git-friendly - state derived from committed files
32+
- Simple - no sync issues between state file and actual files
33+
34+
**Alternatives considered:**
35+
- JSON/SQLite state file: More complex, sync issues, not git-friendly
36+
- Git metadata: Too coupled to git, complex implementation
37+
38+
### Decision: Kahn's Algorithm for Topological Sort
39+
Use Kahn's algorithm for computing build order.
40+
41+
**Rationale:**
42+
- Well-understood, O(V+E) complexity
43+
- Naturally detects cycles during execution
44+
- Produces a stable, deterministic order
45+
46+
### Decision: Glob Pattern Support
47+
Support glob patterns like `specs/*.md` in artifact `generates` field.
48+
49+
**Rationale:**
50+
- Allows multiple files to satisfy a single artifact requirement
51+
- Common pattern for spec directories with multiple files
52+
- Uses standard glob syntax
53+
54+
### Decision: Immutable Completed Set
55+
Represent completion state as an immutable Set of completed artifact IDs.
56+
57+
**Rationale:**
58+
- Functional style, easier to reason about
59+
- State derived fresh each query, no mutation needed
60+
- Clear separation between graph structure and runtime state
61+
- Filesystem can only detect binary existence (complete vs not complete)
62+
63+
**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.
64+
65+
### Decision: Zod for Schema Validation
66+
Use Zod for validating YAML schema structure and deriving TypeScript types.
67+
68+
**Rationale:**
69+
- Already a project dependency (v4.0.17) used in `src/core/schemas/`
70+
- Type inference via `z.infer<>` - single source of truth for types
71+
- Runtime validation with detailed error messages
72+
- Consistent with existing project patterns (`base.schema.ts`, `config-schema.ts`)
73+
74+
**Alternatives considered:**
75+
- Manual validation: More code, error-prone, no type inference
76+
- JSON Schema: Would require additional dependency, less TypeScript integration
77+
- io-ts: Not already in project, steeper learning curve
78+
79+
### Decision: Two-Level Schema Resolution
80+
Schemas resolve from global user data directory, falling back to package built-ins.
81+
82+
**Resolution order:**
83+
1. `${XDG_DATA_HOME:-~/.local/share}/openspec/schemas/<name>.yaml` - Global user override
84+
2. `<package>/schemas/<name>.yaml` - Built-in defaults
85+
86+
**Rationale:**
87+
- Follows XDG Base Directory Specification (schemas are data, not config)
88+
- Mirrors existing `getGlobalConfigDir()` pattern in `src/core/global-paths.ts`
89+
- Built-ins baked into package, never auto-copied
90+
- Users customize by creating files in global data dir
91+
- Simple - no project-level overrides (can add later if needed)
92+
93+
**XDG compliance:**
94+
- Uses `XDG_DATA_HOME` env var when set (all platforms)
95+
- Unix/macOS fallback: `~/.local/share/openspec/`
96+
- Windows fallback: `%LOCALAPPDATA%/openspec/`
97+
98+
**Alternatives considered:**
99+
- Project-level overrides: Added complexity, not needed initially
100+
- Auto-copy to user space: Creates drift, harder to update defaults
101+
- Config directory (`XDG_CONFIG_HOME`): Schemas are workflow definitions (data), not user preferences (config)
102+
103+
### Decision: Template Field Parsed But Not Resolved
104+
The `template` field is required in schema YAML for completeness, but template resolution is deferred to Slice 3.
105+
106+
**Rationale:**
107+
- Slice 1 focuses on "What's Ready?" - dependency and completion queries only
108+
- Template paths are validated syntactically (non-empty string) but not resolved
109+
- Keeps Slice 1 focused and independently testable
110+
111+
### Decision: Cycle Error Format
112+
Cycle errors list all artifact IDs in the cycle for easy debugging.
113+
114+
**Format:** `"Cyclic dependency detected: A → B → C → A"`
115+
116+
**Rationale:**
117+
- Shows the full cycle path, not just that a cycle exists
118+
- Actionable - developer can see exactly which artifacts to fix
119+
- Consistent with Kahn's algorithm which naturally identifies cycle participants
120+
121+
## Data Structures
122+
123+
**Zod Schemas (source of truth):**
124+
125+
```typescript
126+
import { z } from 'zod';
127+
128+
// Artifact definition schema
129+
export const ArtifactSchema = z.object({
130+
id: z.string().min(1, 'Artifact ID is required'),
131+
generates: z.string().min(1), // e.g., "proposal.md" or "specs/*.md"
132+
description: z.string(),
133+
template: z.string(), // path to template file
134+
requires: z.array(z.string()).default([]),
135+
});
136+
137+
// Full schema YAML structure
138+
export const SchemaYamlSchema = z.object({
139+
name: z.string().min(1, 'Schema name is required'),
140+
version: z.number().int().positive(),
141+
description: z.string().optional(),
142+
artifacts: z.array(ArtifactSchema).min(1, 'At least one artifact required'),
143+
});
144+
145+
// Derived TypeScript types
146+
export type Artifact = z.infer<typeof ArtifactSchema>;
147+
export type SchemaYaml = z.infer<typeof SchemaYamlSchema>;
148+
```
149+
150+
**Runtime State (not Zod - internal only):**
151+
152+
```typescript
153+
// Slice 1: Simple completion tracking via filesystem
154+
type CompletedSet = Set<string>;
155+
156+
// Return type for blocked query
157+
interface BlockedArtifacts {
158+
[artifactId: string]: string[]; // artifact → list of unmet dependencies
159+
}
160+
161+
interface ArtifactGraphResult {
162+
completed: string[];
163+
ready: string[];
164+
blocked: BlockedArtifacts;
165+
buildOrder: string[];
166+
}
167+
```
168+
169+
## File Structure
170+
171+
```
172+
src/core/artifact-graph/
173+
├── index.ts # Public exports
174+
├── types.ts # Zod schemas and type definitions
175+
├── graph.ts # ArtifactGraph class
176+
├── state.ts # State detection logic
177+
├── resolver.ts # Schema resolution (global → built-in)
178+
└── schemas/ # Built-in schema definitions (package level)
179+
├── spec-driven.yaml # Default: proposal → specs → design → tasks
180+
└── tdd.yaml # Alternative: tests → implementation → docs
181+
```
182+
183+
**Schema Resolution Paths:**
184+
- Global user override: `${XDG_DATA_HOME:-~/.local/share}/openspec/schemas/<name>.yaml`
185+
- Package built-in: `src/core/artifact-graph/schemas/<name>.yaml` (bundled with package)
186+
187+
## Risks / Trade-offs
188+
189+
| Risk | Mitigation |
190+
|------|------------|
191+
| Glob pattern edge cases | Use well-tested glob library (fast-glob or similar) |
192+
| Cycle detection | Kahn's algorithm naturally fails on cycles; provide clear error |
193+
| Schema evolution | Version field in schema, validate on load |
194+
195+
## Open Questions
196+
197+
None - all questions resolved in Decisions section.
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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Schema Loading
4+
The system SHALL load artifact graph definitions from YAML schema files.
5+
6+
#### Scenario: Valid schema loaded
7+
- **WHEN** a valid schema YAML file is provided
8+
- **THEN** the system returns an ArtifactGraph with all artifacts and dependencies
9+
10+
#### Scenario: Invalid schema rejected
11+
- **WHEN** a schema YAML file is missing required fields
12+
- **THEN** the system throws an error with a descriptive message
13+
14+
#### Scenario: Cyclic dependencies detected
15+
- **WHEN** a schema contains cyclic artifact dependencies
16+
- **THEN** the system throws an error listing the artifact IDs in the cycle
17+
18+
#### Scenario: Invalid dependency reference
19+
- **WHEN** an artifact's `requires` array references a non-existent artifact ID
20+
- **THEN** the system throws an error identifying the invalid reference
21+
22+
#### Scenario: Duplicate artifact IDs rejected
23+
- **WHEN** a schema contains multiple artifacts with the same ID
24+
- **THEN** the system throws an error identifying the duplicate
25+
26+
### Requirement: Build Order Calculation
27+
The system SHALL compute a valid topological build order for artifacts.
28+
29+
#### Scenario: Linear dependency chain
30+
- **WHEN** artifacts form a linear chain (A → B → C)
31+
- **THEN** getBuildOrder() returns [A, B, C]
32+
33+
#### Scenario: Diamond dependency
34+
- **WHEN** artifacts form a diamond (A → B, A → C, B → D, C → D)
35+
- **THEN** getBuildOrder() returns A before B and C, and D last
36+
37+
#### Scenario: Independent artifacts
38+
- **WHEN** artifacts have no dependencies
39+
- **THEN** getBuildOrder() returns them in a stable order
40+
41+
### Requirement: State Detection
42+
The system SHALL detect artifact completion state by scanning the filesystem.
43+
44+
#### Scenario: Simple file exists
45+
- **WHEN** an artifact generates "proposal.md" and the file exists
46+
- **THEN** the artifact is marked as completed
47+
48+
#### Scenario: Simple file missing
49+
- **WHEN** an artifact generates "proposal.md" and the file does not exist
50+
- **THEN** the artifact is not marked as completed
51+
52+
#### Scenario: Glob pattern with files
53+
- **WHEN** an artifact generates "specs/*.md" and the specs/ directory contains .md files
54+
- **THEN** the artifact is marked as completed
55+
56+
#### Scenario: Glob pattern empty
57+
- **WHEN** an artifact generates "specs/*.md" and the specs/ directory is empty or missing
58+
- **THEN** the artifact is not marked as completed
59+
60+
#### Scenario: Missing change directory
61+
- **WHEN** the change directory does not exist
62+
- **THEN** all artifacts are marked as not completed (empty state)
63+
64+
### Requirement: Ready Artifact Query
65+
The system SHALL identify which artifacts are ready to be created based on dependency completion.
66+
67+
#### Scenario: Root artifacts ready initially
68+
- **WHEN** no artifacts are completed
69+
- **THEN** getNextArtifacts() returns artifacts with no dependencies
70+
71+
#### Scenario: Dependent artifact becomes ready
72+
- **WHEN** an artifact's dependencies are all completed
73+
- **THEN** getNextArtifacts() includes that artifact
74+
75+
#### Scenario: Blocked artifacts excluded
76+
- **WHEN** an artifact has uncompleted dependencies
77+
- **THEN** getNextArtifacts() does not include that artifact
78+
79+
### Requirement: Completion Check
80+
The system SHALL determine when all artifacts in a graph are complete.
81+
82+
#### Scenario: All complete
83+
- **WHEN** all artifacts in the graph are in the completed set
84+
- **THEN** isComplete() returns true
85+
86+
#### Scenario: Partially complete
87+
- **WHEN** some artifacts in the graph are not completed
88+
- **THEN** isComplete() returns false
89+
90+
### Requirement: Blocked Query
91+
The system SHALL identify which artifacts are blocked and return all their unmet dependencies.
92+
93+
#### Scenario: Artifact blocked by single dependency
94+
- **WHEN** artifact B requires artifact A and A is not complete
95+
- **THEN** getBlocked() returns `{ B: ['A'] }`
96+
97+
#### Scenario: Artifact blocked by multiple dependencies
98+
- **WHEN** artifact C requires A and B, and only A is complete
99+
- **THEN** getBlocked() returns `{ C: ['B'] }`
100+
101+
#### Scenario: Artifact blocked by all dependencies
102+
- **WHEN** artifact C requires A and B, and neither is complete
103+
- **THEN** getBlocked() returns `{ C: ['A', 'B'] }`
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
## 1. Type Definitions
2+
- [x] 1.1 Create `src/core/artifact-graph/types.ts` with Zod schemas (`ArtifactSchema`, `SchemaYamlSchema`) and inferred types via `z.infer<>`
3+
- [x] 1.2 Define `CompletedSet` (Set<string>), `BlockedArtifacts`, and `ArtifactGraphResult` types for runtime state
4+
5+
## 2. Schema Parser
6+
- [x] 2.1 Create `src/core/artifact-graph/schema.ts` with YAML loading and Zod validation via `.safeParse()`
7+
- [x] 2.2 Implement dependency reference validation (ensure `requires` references valid artifact IDs)
8+
- [x] 2.3 Implement duplicate artifact ID detection
9+
- [x] 2.4 Add cycle detection during schema load (error format: "Cyclic dependency detected: A → B → C → A")
10+
11+
## 3. Artifact Graph Core
12+
- [x] 3.1 Create `src/core/artifact-graph/graph.ts` with ArtifactGraph class
13+
- [x] 3.2 Implement `fromYaml(path)` - load graph from schema file
14+
- [x] 3.3 Implement `getBuildOrder()` - topological sort via Kahn's algorithm
15+
- [x] 3.4 Implement `getArtifact(id)` - retrieve single artifact definition
16+
- [x] 3.5 Implement `getAllArtifacts()` - list all artifacts
17+
18+
## 4. State Detection
19+
- [x] 4.1 Create `src/core/artifact-graph/state.ts` with state detection logic
20+
- [x] 4.2 Implement file existence checking for simple paths
21+
- [x] 4.3 Implement glob pattern matching for multi-file artifacts
22+
- [x] 4.4 Implement `detectCompleted(graph, changeDir)` - scan filesystem and return CompletedSet
23+
- [x] 4.5 Handle missing changeDir gracefully (return empty CompletedSet)
24+
25+
## 5. Ready Calculation
26+
- [x] 5.1 Implement `getNextArtifacts(graph, completed)` - find artifacts with all deps completed
27+
- [x] 5.2 Implement `isComplete(graph, completed)` - check if all artifacts done
28+
- [x] 5.3 Implement `getBlocked(graph, completed)` - return BlockedArtifacts map (artifact → unmet deps)
29+
30+
## 6. Schema Resolution
31+
- [x] 6.1 Create `src/core/artifact-graph/resolver.ts` with schema resolution logic
32+
- [x] 6.2 Add `getGlobalDataDir()` to `src/core/global-config.ts` (XDG_DATA_HOME with platform fallbacks)
33+
- [x] 6.3 Implement `resolveSchema(name)` - global (`${XDG_DATA_HOME}/openspec/schemas/`) → built-in fallback
34+
35+
## 7. Built-in Schemas
36+
- [x] 7.1 Create `src/core/artifact-graph/schemas/spec-driven.yaml` (default: proposal → specs → design → tasks)
37+
- [x] 7.2 Create `src/core/artifact-graph/schemas/tdd.yaml` (alternative: tests → implementation → docs)
38+
39+
## 8. Integration
40+
- [x] 8.1 Create `src/core/artifact-graph/index.ts` with public exports
41+
42+
## 9. Testing
43+
- [x] 9.1 Test: Parse valid schema YAML returns correct artifact graph
44+
- [x] 9.2 Test: Parse invalid schema (missing fields) throws descriptive error
45+
- [x] 9.3 Test: Duplicate artifact IDs throws error
46+
- [x] 9.4 Test: Invalid `requires` reference throws error identifying the invalid ID
47+
- [x] 9.5 Test: Cycle in schema throws error listing cycle path (e.g., "A → B → C → A")
48+
- [x] 9.6 Test: Compute build order returns correct topological ordering (linear chain)
49+
- [x] 9.7 Test: Compute build order handles diamond dependencies correctly
50+
- [x] 9.8 Test: Independent artifacts return in stable order
51+
- [x] 9.9 Test: Empty/missing changeDir returns empty CompletedSet
52+
- [x] 9.10 Test: File existence marks artifact as completed
53+
- [x] 9.11 Test: Glob pattern specs/*.md detected as complete when files exist
54+
- [x] 9.12 Test: Glob pattern with empty directory not marked complete
55+
- [x] 9.13 Test: getNextArtifacts returns only root artifacts when nothing completed
56+
- [x] 9.14 Test: getNextArtifacts includes artifact when all deps completed
57+
- [x] 9.15 Test: getBlocked returns artifact with all unmet dependencies listed
58+
- [x] 9.16 Test: isComplete() returns true when all artifacts completed
59+
- [x] 9.17 Test: isComplete() returns false when some artifacts incomplete
60+
- [x] 9.18 Test: Schema resolution finds global override before built-in
61+
- [x] 9.19 Test: Schema resolution falls back to built-in when no global

0 commit comments

Comments
 (0)