* feat: restructure schemas as directories with templates Move built-in schemas from embedded TypeScript objects to a file-based directory structure. This enables co-located templates alongside schemas. Changes: - Remove builtin-schemas.ts (replaced by file-based schemas) - Add schemas/ directory at package root with spec-driven and tdd schemas - Update resolveSchema() to load from directory structure - Resolution checks user dir → package dir * chore: archive restructure-schema-directories change * docs: update artifact_poc.md for directory-based schema structure Update documentation to reflect the new schema structure where schemas are directories containing schema.yaml and co-located templates/ rather than single .yaml files with separate template directories.
22 KiB
POC-OpenSpec-Core Analysis
Design Decisions & Terminology
Philosophy: Not a Workflow System
This system is not a workflow engine. It's an artifact tracker with dependency awareness.
| What it's NOT | What it IS |
|---|---|
| Linear step-by-step progression | Exploratory, iterative planning |
| Bureaucratic checkpoints | Enablers that unlock possibilities |
| "You must complete step 1 first" | "Here's what you could create now" |
| Form-filling | Fluid document creation |
Key insight: Dependencies are enablers, not gates. You can't meaningfully write a design document if there's no proposal to design from - that's not bureaucracy, it's logic.
Terminology
| Term | Definition | Example |
|---|---|---|
| Change | A unit of work being planned (feature, refactor, migration) | openspec/changes/add-auth/ |
| Schema | An artifact graph definition (what artifacts exist, their dependencies) | spec-driven.yaml |
| Artifact | A node in the graph (a document to create) | proposal, design, specs |
| Template | Instructions/guidance for creating an artifact | templates/proposal.md |
Hierarchy
Schema (defines) ──→ Artifacts (guided by) ──→ Templates
- Schema = the artifact graph (what exists, dependencies)
- Artifact = a document to produce
- Template = instructions for creating that artifact
Schema Variations
Schemas can vary across multiple dimensions:
| Dimension | Examples |
|---|---|
| Philosophy | spec-driven, tdd, prototype-first |
| Version | v1, v2, v3 |
| Language | en, zh, es |
| Custom | team-alpha, experimental |
Schema Resolution (XDG Standard)
Schemas follow the XDG Base Directory Specification with a 2-level resolution:
1. ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml # Global user override
2. <package>/schemas/<name>/schema.yaml # Built-in defaults
Platform-specific paths:
- Unix/macOS:
~/.local/share/openspec/schemas/ - Windows:
%LOCALAPPDATA%/openspec/schemas/ - All platforms:
$XDG_DATA_HOME/openspec/schemas/(when set)
Why XDG?
- Schemas are workflow definitions (data), not user preferences (config)
- Built-ins baked into package, never auto-copied
- Users customize by creating files in global data dir
- Consistent with modern CLI tooling standards
Template Inheritance (2 Levels Max)
Templates are co-located with schemas in a templates/ subdirectory:
1. ${XDG_DATA_HOME}/openspec/schemas/<schema>/templates/<artifact>.md # User override
2. <package>/schemas/<schema>/templates/<artifact>.md # Built-in
Rules:
- User overrides take precedence over package built-ins
- A CLI command shows resolved paths (no guessing)
- No inheritance between schemas (copy if you need to diverge)
- Templates are always co-located with their schema
Why this matters:
- Avoids "where does this come from?" debugging
- No implicit magic that works until it doesn't
- Schema + templates form a cohesive unit
Executive Summary
This is an artifact tracker with dependency awareness that guides iterative development through a structured artifact pipeline. The core innovation is using the filesystem as a database - artifact completion is detected by file existence, making the system stateless and version-control friendly.
The system answers:
- "What artifacts exist for this change?"
- "What could I create next?" (not "what must I create")
- "What's blocking X?" (informational, not prescriptive)
Core Components
1. ArtifactGraph (Slice 1 - COMPLETE)
The dependency graph engine with XDG-compliant schema resolution.
| Responsibility | Approach |
|---|---|
| Model artifacts as a DAG | Artifact with requires: string[] |
| Track completion state | Set<string> for completed artifacts |
| Calculate build order | Kahn's algorithm (topological sort) |
| Find ready artifacts | Check if all dependencies are in completed set |
| Resolve schemas | XDG global → package built-ins |
Key Data Structures (Zod-validated):
// Zod schemas define types + validation
const ArtifactSchema = z.object({
id: z.string().min(1),
generates: z.string().min(1), // e.g., "proposal.md" or "specs/*.md"
description: z.string(),
template: z.string(), // path to template file
requires: z.array(z.string()).default([]),
});
const SchemaYamlSchema = z.object({
name: z.string().min(1),
version: z.number().int().positive(),
description: z.string().optional(),
artifacts: z.array(ArtifactSchema).min(1),
});
// Derived types
type Artifact = z.infer<typeof ArtifactSchema>;
type SchemaYaml = z.infer<typeof SchemaYamlSchema>;
Key Methods:
resolveSchema(name)- Load schema with XDG fallbackArtifactGraph.fromSchema(schema)- Build graph from schemadetectState(graph, changeDir)- Scan filesystem for completiongetNextArtifacts(graph, completed)- Find artifacts ready to creategetBuildOrder(graph)- Topological sort of all artifactsgetBlocked(graph, completed)- Artifacts with unmet dependencies
2. Change Utilities (Slice 2)
Simple utility functions for programmatic change creation. No class, no abstraction layer.
| Responsibility | Approach |
|---|---|
| Create changes | Create dirs under openspec/changes/<name>/ with README |
| Name validation | Enforce kebab-case naming |
Key Paths:
openspec/changes/<name>/ → Change instances with artifacts (project-level)
Key Functions (src/utils/change-utils.ts):
createChange(projectRoot, name, description?)- Create new change directory + READMEvalidateChangeName(name)- Validate kebab-case naming, returns{ valid, error? }
Note: Existing CLI commands (ListCommand, ChangeCommand) already handle listing, path resolution, and existence checks. No need to extract that logic - it works fine as-is.
3. InstructionLoader (Slice 3)
Template resolution and instruction enrichment.
| Responsibility | Approach |
|---|---|
| Resolve templates | XDG 2-level fallback (schema-specific → shared → built-in) |
| Build dynamic context | Gather dependency status, change info |
| Enrich templates | Inject context into base templates |
| Generate status reports | Formatted markdown with progress |
Key Class - ChangeState:
ChangeState {
changeName: string
changeDir: string
graph: ArtifactGraph
completed: Set<string>
// Methods
getNextSteps(): string[]
getStatus(artifactId): ArtifactStatus
isComplete(): boolean
}
Key Functions:
getTemplatePath(artifactId, schemaName?)- Resolve with 2-level fallbackgetEnrichedInstructions(artifactId, projectRoot, changeName?)- Main entry pointgetChangeStatus(projectRoot, changeName?)- Formatted status report
4. CLI (Slice 4)
User interface layer. All commands are deterministic - require explicit --change parameter.
| Command | Function | Status |
|---|---|---|
status --change <id> |
Show change progress (artifact graph) | NEW |
next --change <id> |
Show artifacts ready to create | NEW |
instructions <artifact> --change <id> |
Get enriched instructions for artifact | NEW |
list |
List all changes | EXISTS (openspec change list) |
new <name> |
Create change | NEW (uses createChange()) |
init |
Initialize structure | EXISTS (openspec init) |
templates --change <id> |
Show resolved template paths | NEW |
Note: Commands that operate on a change require --change. Missing parameter → error with list of available changes. Agent infers the change from conversation and passes it explicitly.
Existing CLI commands (not part of this slice):
openspec change list/openspec change show <id>/openspec change validate <id>openspec list --changes/openspec list --specsopenspec view(dashboard)openspec init/openspec archive <change>
5. Claude Commands
Integration layer for Claude Code. Operational commands only - artifact creation via natural language.
| Command | Purpose |
|---|---|
/status |
Show change progress |
/next |
Show what's ready to create |
/run [artifact] |
Execute a specific step (power users) |
/list |
List all changes |
/new <name> |
Create a new change |
/init |
Initialize structure |
Artifact creation: Users say "create the proposal" or "write the tests" in natural language. The agent:
- Infers change from conversation (confirms if uncertain)
- Infers artifact from request
- Calls CLI with explicit
--changeparameter - Creates artifact following instructions
This works for ANY artifact in ANY schema - no new slash commands needed when schemas change.
Note: Legacy commands (/openspec-proposal, /openspec-apply, /openspec-archive) exist in the main project for backward compatibility but are separate from this architecture.
Component Dependency Graph
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌──────────────┐ ┌────────────────────┐ │
│ │ CLI │ ←─shell exec───────│ Claude Commands │ │
│ └──────┬───────┘ └────────────────────┘ │
└─────────┼───────────────────────────────────────────────────┘
│ imports
▼
┌─────────────────────────────────────────────────────────────┐
│ ORCHESTRATION LAYER │
│ ┌────────────────────┐ ┌──────────────────────────┐ │
│ │ InstructionLoader │ │ change-utils (Slice 2) │ │
│ │ (Slice 3) │ │ createChange() │ │
│ └─────────┬──────────┘ │ validateChangeName() │ │
│ │ └──────────────────────────┘ │
└────────────┼────────────────────────────────────────────────┘
│ uses
▼
┌─────────────────────────────────────────────────────────────┐
│ CORE LAYER │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ArtifactGraph (Slice 1) │ │
│ │ │ │
│ │ Schema Resolution (XDG) ──→ Graph ──→ State Detection│ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲
│ reads from
▼
┌─────────────────────────────────────────────────────────────┐
│ PERSISTENCE LAYER │
│ ┌──────────────────┐ ┌────────────────────────────────┐ │
│ │ XDG Schemas │ │ Project Artifacts │ │
│ │ ~/.local/share/ │ │ openspec/changes/<name>/ │ │
│ │ openspec/ │ │ - proposal.md, design.md │ │
│ │ schemas/ │ │ - specs/*.md, tasks.md │ │
│ └──────────────────┘ └────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Key Design Patterns
1. Filesystem as Database
No SQLite, no JSON state files. The existence of proposal.md means proposal is complete.
// State detection is just file existence checking
if (exists(artifactPath)) {
completed.add(artifactId)
}
2. Deterministic CLI, Inferring Agent
CLI layer: Always deterministic - requires explicit --change parameter.
openspec status --change add-auth # explicit, works
openspec status # error: "No change specified"
Agent layer: Infers from conversation, confirms if uncertain, passes explicit --change.
This separation means:
- CLI is pure, testable, no state to corrupt
- Agent handles all "smartness"
- No config.yaml tracking of "active change"
3. XDG-Compliant Schema Resolution
${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml # User override
↓ (not found)
<package>/schemas/<name>/schema.yaml # Built-in
↓ (not found)
Error (schema not found)
4. Two-Level Template Fallback
${XDG_DATA_HOME}/openspec/schemas/<schema>/templates/<artifact>.md # User override
↓ (not found)
<package>/schemas/<schema>/templates/<artifact>.md # Built-in
↓ (not found)
Error (no silent fallback to avoid confusion)
5. Glob Pattern Support
specs/*.md allows multiple files to satisfy a single artifact:
if (artifact.generates.includes("*")) {
const parentDir = changeDir / patternParts[0]
if (exists(parentDir) && hasFiles(parentDir)) {
completed.add(artifactId)
}
}
6. Stateless State Detection
Every command re-scans the filesystem. No cached state to corrupt.
Artifact Pipeline (Default Schema)
The default spec-driven schema:
┌──────────┐
│ proposal │ (no dependencies)
└────┬─────┘
│
▼
┌──────────┐
│ specs │ (requires: proposal)
└────┬─────┘
│
├──────────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ design │ │ │
│ │◄──┤ proposal │
└────┬─────┘ └──────────┘
│ (requires: proposal, specs)
▼
┌──────────┐
│ tasks │ (requires: design)
└──────────┘
Other schemas (TDD, prototype-first) would have different graphs.
Implementation Order
Structured as vertical slices - each slice is independently testable.
Slice 1: "What's Ready?" (Core Query) ✅ COMPLETE
Delivers: Types + Graph + State Detection + Schema Resolution
Implementation: src/core/artifact-graph/
types.ts- Zod schemas and derived TypeScript typesschema.ts- YAML parsing with Zod validationgraph.ts- ArtifactGraph class with topological sortstate.ts- Filesystem-based state detectionresolver.ts- XDG-compliant schema resolutionbuiltin-schemas.ts- Package-bundled default schemas
Key decisions made:
- Zod for schema validation (consistent with project)
- XDG for global schema overrides
Set<string>for completion state (immutable, functional)inProgressandfailedstates deferred (require external tracking)
Slice 2: "Change Creation Utilities"
Delivers: Utility functions for programmatic change creation
Scope:
createChange(projectRoot, name, description?)→ creates directory + READMEvalidateChangeName(name)→ kebab-case pattern enforcement
Not in scope (already exists in CLI commands):
listChanges()→ exists inListCommandandChangeCommand.getActiveChanges()getChangePath()→ simplepath.join()inlinechangeExists()→ simplefs.access()inlineisInitialized()→ simple directory check inline
Why simplified: Extracting existing CLI logic into a class would require similar refactoring of SpecCommand for consistency. The existing code works fine (~15 lines each). Only truly new functionality is createChange() + name validation.
Slice 3: "Get Instructions" (Enrichment)
Delivers: Template resolution + context injection
Testable behaviors:
- Template fallback: schema-specific → shared → built-in → error
- Context injection: completed deps show ✓, missing show ✗
- Output path shown correctly based on change directory
Slice 4: "CLI + Integration"
Delivers: New artifact graph commands (builds on existing CLI)
New commands:
status --change <id>- Show artifact completion statenext --change <id>- Show ready-to-create artifactsinstructions <artifact> --change <id>- Get enriched templatetemplates --change <id>- Show resolved pathsnew <name>- Create change (wrapper forcreateChange())
Already exists (not in scope):
openspec change list/show/validate- change managementopenspec list --changes/--specs- listingopenspec view- dashboardopenspec init- initialization
Testable behaviors:
- Each new command produces expected output
- Commands compose correctly (status → next → instructions flow)
- Error handling for missing changes, invalid artifacts, etc.
Directory Structure
# Global (XDG paths - user overrides)
~/.local/share/openspec/ # Unix/macOS ($XDG_DATA_HOME/openspec/)
%LOCALAPPDATA%/openspec/ # Windows
└── schemas/ # Schema overrides
└── custom-workflow/ # User-defined schema directory
├── schema.yaml # Schema definition
└── templates/ # Co-located templates
└── proposal.md
# Package (built-in defaults)
<package>/
└── schemas/ # Built-in schema definitions
├── spec-driven/ # Default: proposal → specs → design → tasks
│ ├── schema.yaml
│ └── templates/
│ ├── proposal.md
│ ├── design.md
│ ├── spec.md
│ └── tasks.md
└── tdd/ # TDD: tests → implementation → docs
├── schema.yaml
└── templates/
├── test.md
├── implementation.md
├── spec.md
└── docs.md
# Project (change instances)
openspec/
└── changes/ # Change instances
├── add-auth/
│ ├── README.md # Auto-generated on creation
│ ├── proposal.md # Created artifacts
│ ├── design.md
│ └── specs/
│ └── *.md
├── refactor-db/
│ └── ...
└── archive/ # Completed changes
└── 2025-01-01-add-auth/
.claude/
├── settings.local.json # Permissions
└── commands/ # Slash commands
└── *.md
Schema YAML Format
# Built-in: <package>/schemas/spec-driven/schema.yaml
# Or user override: ~/.local/share/openspec/schemas/spec-driven/schema.yaml
name: spec-driven
version: 1
description: Specification-driven development
artifacts:
- id: proposal
generates: "proposal.md"
description: "Create project proposal document"
template: "proposal.md" # resolves from co-located templates/ directory
requires: []
- id: specs
generates: "specs/*.md" # glob pattern
description: "Create technical specification documents"
template: "specs.md"
requires:
- proposal
- id: design
generates: "design.md"
description: "Create design document"
template: "design.md"
requires:
- proposal
- specs
- id: tasks
generates: "tasks.md"
description: "Create tasks breakdown document"
template: "tasks.md"
requires:
- design
Summary
| Layer | Component | Responsibility | Status |
|---|---|---|---|
| Core | ArtifactGraph | Pure dependency logic + XDG schema resolution | ✅ Slice 1 COMPLETE |
| Utils | change-utils | Change creation + name validation only | Slice 2 (new functionality only) |
| Core | InstructionLoader | Template resolution + enrichment | Slice 3 (all new) |
| Presentation | CLI | New artifact graph commands | Slice 4 (new commands only) |
| Integration | Claude Commands | AI assistant glue | Slice 4 |
What already exists (not in this proposal):
getActiveChangeIds()insrc/utils/item-discovery.ts- list changesChangeCommand.list/show/validate()insrc/commands/change.tsListCommand.execute()insrc/core/list.tsViewCommand.execute()insrc/core/view.ts- dashboardsrc/core/init.ts- initializationsrc/core/archive.ts- archiving
Key Principles:
- Filesystem IS the database - stateless, version-control friendly
- Dependencies are enablers - show what's possible, don't force order
- Deterministic CLI, inferring agent - CLI requires explicit
--change, agent infers from context - XDG-compliant paths - schemas and templates use standard user data directories
- 2-level inheritance - user override → package built-in (no deeper)
- Schemas are versioned - support variations by philosophy, version, language