OpenSpec 源码剖析
从 CLI 入口到核心算法,深入理解 AI-native 规格驱动开发框架的实现原理
项目概览
OpenSpec 技术画像
| 属性 | 值 |
|---|---|
| 定位 | AI-native 规格驱动开发 (SDD) 框架 |
| 语言 | TypeScript (ESM 模块) |
| 运行时 | Node.js ≥ 20.19.0 |
| 包管理 | pnpm |
| 代码量 | ~22,000 行 TypeScript,130+ 源文件 |
| 测试框架 | Vitest |
| CLI 框架 | Commander.js |
| 核心依赖 | Zod (校验)、fast-glob (文件匹配)、chalk (输出)、ora (loading)、yaml (解析) |
核心设计理念
Graph-Driven
用有向无环图 (DAG) 管理 artifact 依赖,Kahn 算法保证拓扑序
Schema-First
YAML schema 定义工作流,三级解析(项目 > 用户 > 内置)
Adapter Pattern
25+ 工具适配器,统一内容模型 → 多格式输出
File-System State
无数据库,文件存在即完成,Git 友好
源码目录结构
完整源码树
OpenSpec/
├── bin/openspec.js # CLI 启动入口 (shebang 脚本)
├── src/ # 核心源码 (~130 .ts 文件)
│ ├── cli/index.ts # CLI 命令注册与路由 (Commander.js)
│ ├── commands/ # 命令实现
│ │ ├── workflow/ # Workflow 命令族
│ │ │ ├── index.ts # status/instructions/templates/schemas/new
│ │ │ └── ...
│ │ ├── change.ts # 变更管理 (legacy)
│ │ ├── spec.ts # Spec 命令
│ │ ├── config.ts # 配置命令
│ │ ├── validate.ts # 验证命令
│ │ ├── schema.ts # Schema 管理 (fork/init/validate)
│ │ ├── show.ts # 详情展示
│ │ ├── completion.ts # Shell 补全
│ │ └── feedback.ts # 反馈提交
│ ├── core/ # 核心业务逻辑
│ │ ├── artifact-graph/ # ★ Artifact 依赖图引擎
│ │ │ ├── graph.ts # ArtifactGraph 类 + Kahn 算法
│ │ │ ├── resolver.ts # Schema 三级解析
│ │ │ ├── state.ts # 完成状态检测
│ │ │ ├── instruction-loader.ts # 指令富化引擎
│ │ │ ├── schema.ts # YAML 解析 + Zod 校验
│ │ │ └── types.ts # 类型定义
│ │ ├── command-generation/ # ★ 多工具命令生成
│ │ │ ├── adapters/ # 25+ 工具适配器
│ │ │ │ ├── claude.ts # Claude Code 适配器
│ │ │ │ ├── cursor.ts # Cursor 适配器
│ │ │ │ ├── factory.ts # 适配器工厂
│ │ │ │ └── ... # 其他工具
│ │ │ ├── generator.ts # 命令文件生成器
│ │ │ ├── registry.ts # 工具注册表
│ │ │ └── types.ts # CommandContent 等类型
│ │ ├── parsers/ # ★ Markdown 解析
│ │ │ ├── markdown-parser.ts # Spec 文件解析
│ │ │ ├── change-parser.ts # Change 文件解析
│ │ │ └── requirement-blocks.ts # Delta Spec 解析
│ │ ├── validation/ # ★ 验证系统
│ │ │ ├── validator.ts # Validator 类 (Zod + 自定义规则)
│ │ │ ├── types.ts # ValidationIssue 等类型
│ │ │ └── constants.ts # 验证常量
│ │ ├── templates/ # Skill/Command 模板
│ │ │ └── workflows/ # 11 个工作流模板
│ │ │ ├── propose.ts
│ │ │ ├── apply.ts
│ │ │ ├── archive.ts
│ │ │ └── ...
│ │ ├── shared/ # 共享逻辑
│ │ │ ├── skill-generation.ts # Skill 内容生成
│ │ │ └── tool-detection.ts # AI 工具发现
│ │ ├── schemas/ # Zod Schema 定义
│ │ ├── completions/ # Shell 补全生成器
│ │ ├── init.ts # 项目初始化
│ │ ├── archive.ts # 归档命令
│ │ ├── specs-apply.ts # Delta Spec 合并算法
│ │ ├── update.ts # 配置更新
│ │ ├── list.ts # 列表命令
│ │ ├── view.ts # 交互式仪表盘
│ │ ├── config.ts # 工具常量 (AI_TOOLS)
│ │ ├── project-config.ts # config.yaml 解析
│ │ ├── global-config.ts # XDG 全局配置
│ │ ├── profiles.ts # 工作流 Profile 系统
│ │ └── migration.ts # 旧格式迁移
│ ├── utils/ # 工具函数
│ │ ├── file-system.ts # 跨平台文件操作
│ │ ├── change-metadata.ts # .openspec.yaml 解析
│ │ ├── task-progress.ts # checkbox 进度追踪
│ │ └── interactive.ts # 交互式提示
│ ├── telemetry/ # 匿名遥测 (PostHog)
│ └── ui/ # 欢迎屏幕等 UI 组件
├── schemas/ # 内置工作流 Schema
│ └── spec-driven/
│ ├── schema.yaml # 默认 Schema 定义
│ └── templates/ # Artifact 模板
│ ├── proposal.md
│ ├── spec.md
│ ├── design.md
│ └── tasks.md
├── test/ # Vitest 测试套件 (50+ 文件)
└── docs/ # 文档
模块依赖关系
模块调用图
CLI 层 (src/cli/index.ts)
│
├──→ commands/* 命令实现层
│ ├── workflow/* → core/artifact-graph/* (查询 artifact 状态)
│ ├── validate.ts → core/validation/validator.ts
│ ├── schema.ts → core/artifact-graph/resolver.ts
│ └── show.ts → core/parsers/*
│
├──→ core/init.ts 项目初始化
│ ├──→ command-generation/* 命令文件生成
│ │ ├── generator.ts → adapters/* (25+ 适配器)
│ │ └── registry.ts → 工具注册表
│ └──→ shared/skill-generation.ts Skill 内容生成
│ └──→ templates/workflows/* 工作流模板
│
├──→ core/archive.ts 归档流程
│ ├──→ core/specs-apply.ts Delta Spec 合并
│ │ └──→ parsers/requirement-blocks.ts 解析 delta 格式
│ └──→ core/validation/validator.ts 归档前验证
│
└──→ core/artifact-graph/* ★ 核心引擎
├── graph.ts ArtifactGraph + Kahn 算法
├── state.ts 文件系统状态检测
├── resolver.ts Schema 三级解析
├── instruction-loader.ts 指令富化
└── types.ts Zod 类型定义
数据流向
schema.yaml ──→ ArtifactGraph ──→ getBuildOrder()
│ │
│ getNextArtifacts()
│ │
config.yaml ──→ instructions ◀── template + instruction
│
▼
AI 创作 artifact
│
▼
detectCompleted() ──→ 文件系统扫描
│
▼
archive ──→ Delta Spec 合并 ──→ 主 specs/
CLI 入口与路由
入口文件:src/cli/index.ts
OpenSpec 使用 Commander.js 作为 CLI 框架,在一个文件中注册所有命令和全局钩子。
// src/cli/index.ts (核心结构)
import { Command } from 'commander';
const program = new Command();
program
.name('openspec')
.description('AI-native system for spec-driven development')
.version(version);
// 全局钩子:每个命令执行前/后自动触发
program.hook('preAction', async (thisCommand, actionCommand) => {
// 1. 颜色检测
// 2. 遥测通知(首次运行提示)
// 3. 命令追踪
const commandPath = getCommandPath(actionCommand);
await trackCommand(commandPath, version);
});
program.hook('postAction', async () => {
await shutdown(); // 关闭遥测客户端
});
命令路径解析
嵌套命令通过 getCommandPath 函数转化为 : 分隔的路径:
// openspec change show → "change:show"
// openspec schema fork → "schema:fork"
function getCommandPath(command: Command): string {
const names: string[] = [];
let current: Command | null = command;
while (current) {
const name = current.name();
if (name && name !== 'openspec') {
names.unshift(name);
}
current = current.parent;
}
return names.join(':') || 'openspec';
}
命令注册一览
| 命令 | 实现文件 | 职责 |
|---|---|---|
init [path] | core/init.ts | 项目初始化,生成 AI 工具配置 |
update [path] | core/update.ts | 升级后重新生成配置文件 |
list | core/list.ts | 列出变更或 specs |
view | core/view.ts | 交互式仪表盘 |
archive [name] | core/archive.ts | 归档变更 + 合并 specs |
validate [item] | commands/validate.ts | 验证格式 |
show [item] | commands/show.ts | 查看详情 |
status | commands/workflow/ | artifact 状态 |
instructions | commands/workflow/ | 获取富化指令 |
new change | commands/workflow/ | 创建变更目录 |
schema fork|init | commands/schema.ts | Schema 管理 |
config profile|delivery | commands/config.ts | 全局配置 |
completion | commands/completion.ts | Shell 补全 |
init 初始化流程
初始化执行链路:core/init.ts
openspec init [path] --tools claude,cursor
│
├── 1. 检测遗留文件 (detectLegacyArtifacts)
│ → 发现旧格式文件则提示清理
│
├── 2. 交互式工具选择(非 --tools 模式)
│ → isInteractive() 检测终端
│ → 列出 AI_TOOLS 供选择
│
├── 3. 创建 openspec/ 目录结构
│ ├── openspec/config.yaml
│ ├── openspec/specs/
│ ├── openspec/changes/
│ └── openspec/agent-instructions.md
│
├── 4. 生成 Skill 文件
│ ├── getSkillTemplates() → 获取工作流模板
│ ├── generateSkillContent() → 渲染 YAML frontmatter
│ └── 写入各工具的 skills 目录
│
├── 5. 生成 Command 文件
│ ├── getCommandContents() → 获取命令内容
│ ├── CommandAdapterRegistry → 查找适配器
│ └── generateCommands() → 按工具格式写入
│
└── 6. 运行旧格式迁移
→ migrateIfNeeded()
工具常量定义:core/config.ts
AI_TOOLS 是一个数组,定义了所有支持的 AI 工具:
export const AI_TOOLS: AIToolOption[] = [
{ name: 'Claude Code', value: 'claude', skillsDir: '.claude/skills' },
{ name: 'Cursor', value: 'cursor', skillsDir: '.cursor/skills' },
{ name: 'GitHub Copilot', value: 'github-copilot', skillsDir: '.github/prompts' },
{ name: 'Windsurf', value: 'windsurf', skillsDir: '.windsurf/workflows' },
{ name: 'Gemini CLI', value: 'gemini', skillsDir: '.gemini/commands' },
// ... 20+ 更多工具
];
工作流 → Skill 目录映射
// core/init.ts
const WORKFLOW_TO_SKILL_DIR: Record<string, string> = {
'explore': 'openspec-explore',
'new': 'openspec-new-change',
'continue': 'openspec-continue-change',
'apply': 'openspec-apply-change',
'ff': 'openspec-ff-change',
'sync': 'openspec-sync-specs',
'archive': 'openspec-archive-change',
'bulk-archive': 'openspec-bulk-archive-change',
'verify': 'openspec-verify-change',
'onboard': 'openspec-onboard',
'propose': 'openspec-propose',
};
每个工作流对应一个 skill 目录,init 时根据 profile (core/custom) 选择生成哪些。
Workflow 命令族
commands/workflow/index.ts
Workflow 命令是 OpenSpec 的核心交互接口,所有命令共享同一套 artifact-graph 引擎:
// status 命令:查询 artifact 状态
export async function statusCommand(options: StatusOptions) {
const context = loadChangeContext(projectRoot, changeName, options.schema);
const status = formatChangeStatus(context);
// 输出 JSON 或人类可读格式
}
// instructions 命令:获取富化指令
export async function instructionsCommand(
artifactId: string,
options: InstructionsOptions
) {
const context = loadChangeContext(projectRoot, changeName, options.schema);
const instructions = generateInstructions(context, artifactId, projectRoot);
// 返回 template + instruction + context + rules + dependencies
}
// new change 命令:创建变更目录
export async function newChangeCommand(
changeName: string,
options: NewChangeOptions
) {
// 创建 openspec/changes/<name>/
// 写入 .openspec.yaml 元数据
}
命令间的数据流
new change → 创建目录 + .openspec.yaml
│
▼
status → loadChangeContext() → ArtifactGraph + detectCompleted()
│
▼
instructions → generateInstructions() → 富化指令 JSON
│
▼
(AI 创建 artifact 文件)
│
▼
status → detectCompleted() 重新检测 → 更新状态
│
▼
archive → 合并 Delta Specs + 移动到 archive/
Artifact 依赖图引擎
ArtifactGraph 类:core/artifact-graph/graph.ts
ArtifactGraph 是 OpenSpec 的核心数据结构,将 schema.yaml 中定义的 artifact 及其依赖关系建模为有向无环图 (DAG)。
export class ArtifactGraph {
private artifacts: Map<string, Artifact>;
private schema: SchemaYaml;
// 私有构造函数,通过静态工厂方法创建
private constructor(schema: SchemaYaml) {
this.schema = schema;
// 将 artifact 数组转为 Map,以 id 为 key
this.artifacts = new Map(schema.artifacts.map(a => [a.id, a]));
}
// 三种工厂方法
static fromYaml(filePath: string): ArtifactGraph // 从文件路径
static fromYamlContent(yamlContent: string): ArtifactGraph // 从字符串
static fromSchema(schema: SchemaYaml): ArtifactGraph // 从已解析对象
}
Artifact 类型定义
// core/artifact-graph/types.ts
export interface Artifact {
id: string; // 唯一标识,如 "proposal"
generates: string; // 产出路径,如 "proposal.md" 或 "specs/**/*.md"
description: string; // 描述
template: string; // 模板文件相对路径
instruction?: string; // 创作指导
requires: string[]; // 依赖的 artifact ID 列表
}
export interface SchemaYaml {
name: string;
version: number;
description: string;
artifacts: Artifact[];
apply?: {
requires: string[]; // apply 前置条件
tracks: string; // 进度追踪文件
instruction?: string; // 实现指导
};
}
export type CompletedSet = Set<string>;
export type BlockedArtifacts = Record<string, string[]>;
核心查询方法
| 方法 | 入参 | 返回 | 用途 |
|---|---|---|---|
getBuildOrder() | 无 | string[] | Kahn 算法拓扑排序 |
getNextArtifacts(completed) | 已完成集合 | string[] | 可创建的 artifact |
isComplete(completed) | 已完成集合 | boolean | 全部完成检测 |
getBlocked(completed) | 已完成集合 | BlockedArtifacts | 阻塞原因查询 |
Kahn 拓扑排序算法
getBuildOrder() 源码解析
OpenSpec 使用 Kahn 算法计算 artifact 的构建顺序。该算法通过维护入度计数器,从入度为 0 的节点开始逐步剥离:
getBuildOrder(): string[] {
// Step 1: 初始化入度和反向邻接表
const inDegree = new Map<string, number>();
const dependents = new Map<string, string[]>();
for (const artifact of this.artifacts.values()) {
inDegree.set(artifact.id, artifact.requires.length); // 入度 = 依赖数
dependents.set(artifact.id, []); // 反向边列表
}
// Step 2: 构建反向邻接表(谁依赖谁 → 被依赖者指向依赖者)
for (const artifact of this.artifacts.values()) {
for (const req of artifact.requires) {
dependents.get(req)!.push(artifact.id);
}
}
// Step 3: 收集入度为 0 的根节点(排序保证确定性)
const queue = [...this.artifacts.keys()]
.filter(id => inDegree.get(id) === 0)
.sort();
const result: string[] = [];
// Step 4: BFS 逐层剥离
while (queue.length > 0) {
const current = queue.shift()!; // 取出入度为 0 的节点
result.push(current);
// 减少所有后继节点的入度
const newlyReady: string[] = [];
for (const dep of dependents.get(current)!) {
const newDegree = inDegree.get(dep)! - 1;
inDegree.set(dep, newDegree);
if (newDegree === 0) {
newlyReady.push(dep); // 入度变为 0,加入队列
}
}
queue.push(...newlyReady.sort()); // 排序保证确定性输出
}
return result; // 拓扑序列
}
算法执行过程可视化
以默认 spec-driven schema 为例:
依赖关系:
proposal → (无依赖)
specs → [proposal]
design → [proposal]
tasks → [specs, design]
初始入度:
proposal=0 specs=1 design=1 tasks=2
第 1 轮:取出 proposal (入度=0)
→ specs 入度 1→0, design 入度 1→0
→ queue: [design, specs] (排序后)
第 2 轮:取出 design (入度=0)
→ tasks 入度 2→1
→ queue: [specs]
第 3 轮:取出 specs (入度=0)
→ tasks 入度 1→0
→ queue: [tasks]
第 4 轮:取出 tasks (入度=0)
→ queue: []
结果:[proposal, design, specs, tasks]
.sort() 调用——初始队列和新就绪节点都排序,确保相同输入总是产出相同顺序。这对 AI 生成的一致性至关重要。
getNextArtifacts() — 就绪节点查询
getNextArtifacts(completed: CompletedSet): string[] {
const ready: string[] = [];
for (const artifact of this.artifacts.values()) {
if (completed.has(artifact.id)) continue; // 已完成,跳过
// 所有依赖都在 completed 中 → ready
const allDepsCompleted = artifact.requires.every(
req => completed.has(req)
);
if (allDepsCompleted) {
ready.push(artifact.id);
}
}
return ready.sort(); // 确定性排序
}
时间复杂度:O(V × D),V = artifact 数量,D = 平均依赖数。对于典型的 4-6 个 artifact 场景,性能微不足道。
完成状态检测
基于文件系统的状态机:core/artifact-graph/state.ts
OpenSpec 的一个精妙设计:不用数据库,不用状态文件,文件存在即完成。
export function detectCompleted(
graph: ArtifactGraph,
changeDir: string
): CompletedSet {
const completed = new Set<string>();
// 目录不存在则返回空集
if (!fs.existsSync(changeDir)) return completed;
for (const artifact of graph.getAllArtifacts()) {
if (isArtifactComplete(artifact.generates, changeDir)) {
completed.add(artifact.id);
}
}
return completed;
}
两种检测模式
function isArtifactComplete(generates: string, changeDir: string): boolean {
const fullPattern = path.join(changeDir, generates);
if (isGlobPattern(generates)) {
// Glob 模式:specs/**/*.md → 有匹配文件即完成
return hasGlobMatches(fullPattern);
}
// 简单路径:proposal.md → 文件存在即完成
return fs.existsSync(fullPattern);
}
function isGlobPattern(pattern: string): boolean {
// 检测 *, ?, [ 三种通配符
return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
}
function hasGlobMatches(pattern: string): boolean {
// 跨平台:Windows 反斜杠 → POSIX 正斜杠
const normalizedPattern = FileSystemUtils.toPosixPath(pattern);
const matches = fg.sync(normalizedPattern, { onlyFiles: true });
return matches.length > 0;
}
设计决策:为什么用文件系统而非数据库
| 优势 | 说明 |
|---|---|
| 零依赖 | 不需要数据库、Redis 或任何外部服务 |
| Git 友好 | 状态天然跟随文件进入版本控制 |
| 无状态同步 | 多人协作时 git pull 即同步状态,无需额外协议 |
| 可审计 | 文件存在/不存在即是证据,无需查日志 |
| 手动可干预 | 删除文件即重置状态,无需特殊命令 |
Schema 三级解析
解析优先级:core/artifact-graph/resolver.ts
Schema 解析遵循就近优先原则,三级覆盖:
优先级从高到低:
1. 项目本地 (Project-Local)
→ <projectRoot>/openspec/schemas/<name>/schema.yaml
→ 项目级自定义,Git 随项目走
2. 用户覆盖 (User Override)
→ $XDG_DATA_HOME/openspec/schemas/<name>/schema.yaml
→ ~/.local/share/openspec/schemas/... (Linux/macOS 默认)
→ 个人偏好,跨项目生效
3. 包内置 (Package Built-in)
→ <npm-package>/schemas/<name>/schema.yaml
→ 默认随 npm 包发布,兜底
getSchemaDir() 解析实现
export function getSchemaDir(
name: string,
projectRoot?: string
): string | null {
// 1. 检查项目本地目录
if (projectRoot) {
const projectDir = path.join(
getProjectSchemasDir(projectRoot), name
);
if (fs.existsSync(path.join(projectDir, 'schema.yaml'))) {
return projectDir;
}
}
// 2. 检查用户覆盖目录
const userDir = path.join(getUserSchemasDir(), name);
if (fs.existsSync(path.join(userDir, 'schema.yaml'))) {
return userDir;
}
// 3. 检查包内置目录
const packageDir = path.join(getPackageSchemasDir(), name);
if (fs.existsSync(path.join(packageDir, 'schema.yaml'))) {
return packageDir;
}
return null; // 未找到
}
路径定位函数
// 包内置 schemas 路径:通过 import.meta.url 定位
export function getPackageSchemasDir(): string {
const currentFile = fileURLToPath(import.meta.url);
// 从 dist/core/artifact-graph/ 向上导航到 schemas/
return path.join(path.dirname(currentFile), '..', '..', '..', 'schemas');
}
// 用户 schemas 路径:遵循 XDG 规范
export function getUserSchemasDir(): string {
return path.join(getGlobalDataDir(), 'schemas');
// → ~/.local/share/openspec/schemas/ (Linux)
// → ~/Library/Application Support/openspec/schemas/ (macOS)
}
// 项目本地 schemas 路径
export function getProjectSchemasDir(projectRoot: string): string {
return path.join(projectRoot, 'openspec', 'schemas');
}
Schema 变更解析链
当执行命令时,schema 名称的解析经过多层:
CLI 参数 --schema <name>
→ 有值则直接使用
→ 无值则:
.openspec.yaml 中的 schema 字段
→ 有值则使用
→ 无值则:
openspec/config.yaml 中的 schema 字段
→ 有值则使用
→ 无值则:默认 "spec-driven"
// utils/change-metadata.ts
export function resolveSchemaForChange(
changeDir: string,
explicitSchema?: string
): string {
if (explicitSchema) return explicitSchema;
// 尝试从 .openspec.yaml 读取
const metadata = readChangeMetadata(changeDir);
if (metadata?.schema) return metadata.schema;
return 'spec-driven'; // 默认值
}
指令富化引擎
generateInstructions():核心指令组装器
位于 core/artifact-graph/instruction-loader.ts,这个函数将 schema 指令、项目配置和模板组装成一份完整的 AI 创作指令:
export function generateInstructions(
context: ChangeContext,
artifactId: string,
projectRoot?: string
): ArtifactInstructions {
// 1. 查找 artifact 定义
const artifact = context.graph.getArtifact(artifactId);
if (!artifact) throw new Error(`Artifact '${artifactId}' not found`);
// 2. 加载模板
const templateContent = loadTemplate(
context.schemaName, artifact.template, context.projectRoot
);
// 3. 计算依赖信息
const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
// 4. 计算解锁信息(完成后哪些 artifact 变为 ready)
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
// 5. 读取项目配置
let projectConfig = null;
try {
projectConfig = readProjectConfig(effectiveProjectRoot);
} catch { /* 无配置则跳过 */ }
// 6. 验证 rules 中的 artifact ID 是否有效
if (projectConfig?.rules) {
const validArtifactIds = new Set(
context.graph.getAllArtifacts().map(a => a.id)
);
const warnings = validateConfigRules(
projectConfig.rules, validArtifactIds, context.schemaName
);
// 每个 warning 只显示一次(Session 级缓存)
for (const warning of warnings) {
if (!shownWarnings.has(warning)) {
console.warn(warning);
shownWarnings.add(warning);
}
}
}
// 7. 提取 context 和 rules(不合并到 template 中!)
const configContext = projectConfig?.context?.trim() || undefined;
const configRules = projectConfig?.rules?.[artifactId] || undefined;
// 8. 组装返回
return {
changeName, artifactId, schemaName, changeDir,
outputPath: artifact.generates,
description: artifact.description,
instruction: artifact.instruction, // 来自 schema
context: configContext, // 来自 config.yaml
rules: configRules, // 来自 config.yaml
template: templateContent, // 来自 templates/
dependencies, // 已完成的依赖
unlocks, // 将解锁的 artifact
};
}
依赖信息组装
function getDependencyInfo(
artifact: Artifact,
graph: ArtifactGraph,
completed: CompletedSet
): DependencyInfo[] {
return artifact.requires.map(id => {
const depArtifact = graph.getArtifact(id);
return {
id,
done: completed.has(id), // 是否已完成
path: depArtifact?.generates ?? id, // 文件路径
description: depArtifact?.description ?? '',
};
});
}
// 解锁计算:反向查找谁的 requires 包含当前 artifact
function getUnlockedArtifacts(graph: ArtifactGraph, artifactId: string): string[] {
return graph.getAllArtifacts()
.filter(a => a.requires.includes(artifactId))
.map(a => a.id)
.sort();
}
ChangeContext 接口
export interface ChangeContext {
graph: ArtifactGraph; // 依赖图实例
completed: CompletedSet; // 已完成 artifact ID 集合
schemaName: string; // 使用的 schema 名称
changeName: string; // 变更名称
changeDir: string; // 变更目录完整路径
projectRoot: string; // 项目根目录
}
// 由 loadChangeContext() 一次性加载
export function loadChangeContext(
projectRoot: string,
changeName: string,
schemaName?: string
): ChangeContext {
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName);
const schema = resolveSchema(resolvedSchemaName, projectRoot);
const graph = ArtifactGraph.fromSchema(schema);
const completed = detectCompleted(graph, changeDir);
return { graph, completed, schemaName: resolvedSchemaName,
changeName, changeDir, projectRoot };
}
Markdown 解析器
Requirement 块解析器:parsers/requirement-blocks.ts
这是 Delta Spec 合并和验证的基础——将 Markdown 中的 Requirement 块解析为结构化数据:
export interface RequirementBlock {
headerLine: string; // '### Requirement: Something'
name: string; // 'Something'
raw: string; // 完整块(header + body)
}
export interface RequirementsSectionParts {
before: string; // ## Requirements 之前的内容
headerLine: string; // '## Requirements' 行
preamble: string; // header 和第一个 Requirement 之间的内容
bodyBlocks: RequirementBlock[]; // 解析后的 Requirement 块
after: string; // ## Requirements 之后的内容
}
extractRequirementsSection() 核心逻辑
export function extractRequirementsSection(content: string):
RequirementsSectionParts {
const lines = normalizeLineEndings(content).split('\n');
// 1. 定位 "## Requirements" header
const reqHeaderIndex = lines.findIndex(
l => /^##\s+Requirements\s*$/i.test(l)
);
// 2. 如果不存在,创建空 section
if (reqHeaderIndex === -1) {
return { before: content, headerLine: '## Requirements',
preamble: '', bodyBlocks: [], after: '\n' };
}
// 3. 找到 section 结束位置(下一个 ## 级别标题)
let endIndex = lines.length;
for (let i = reqHeaderIndex + 1; i < lines.length; i++) {
if (/^##\s+/.test(lines[i])) { endIndex = i; break; }
}
// 4. 解析 section 内的 Requirement 块
const sectionBodyLines = lines.slice(reqHeaderIndex + 1, endIndex);
const blocks: RequirementBlock[] = [];
// 跳过 preamble 直到第一个 ### Requirement:
// 然后按 ### Requirement: 切分块
// ...
return { before, headerLine, preamble, bodyBlocks: blocks, after };
}
Delta Spec 解析:parseDeltaSpec()
export interface DeltaPlan {
added: RequirementBlock[]; // 新增
modified: RequirementBlock[]; // 修改(完整替换内容)
removed: string[]; // 删除(Requirement 名称)
renamed: Array<{ from: string; to: string }>; // 重命名
sectionPresence: { // 各分区是否存在
added: boolean; modified: boolean;
removed: boolean; renamed: boolean;
};
}
export function parseDeltaSpec(content: string): DeltaPlan {
// 按 ## ADDED/MODIFIED/REMOVED/RENAMED Requirements 分区
// 每个分区内再按 ### Requirement: 切分块
// 返回结构化的 DeltaPlan
}
Delta Spec 合并算法
buildUpdatedSpec():core/specs-apply.ts
归档时的核心算法——将 delta spec 合并到主 spec:
export async function buildUpdatedSpec(
update: SpecUpdate,
changeName: string
): Promise<{ rebuilt: string; counts: {...} }> {
// 1. 读取 delta spec 并解析
const changeContent = await fs.readFile(update.source, 'utf-8');
const plan = parseDeltaSpec(changeContent);
// 2. 验证无重复 Requirement 名称
const addedNames = new Set<string>();
for (const add of plan.added) {
const name = normalizeRequirementName(add.name);
if (addedNames.has(name)) throw new Error(`Duplicate: ${add.name}`);
addedNames.add(name);
}
// 3. 读取主 spec 或创建空文件
let existingContent = '';
if (update.exists) {
existingContent = await fs.readFile(update.target, 'utf-8');
}
// 4. 提取主 spec 的 Requirements section
const parts = extractRequirementsSection(existingContent);
const existingBlocks = new Map(
parts.bodyBlocks.map(b => [normalizeRequirementName(b.name), b])
);
// 5. 执行 REMOVED — 从 Map 中删除
for (const name of plan.removed) {
existingBlocks.delete(normalizeRequirementName(name));
}
// 6. 执行 RENAMED — 修改 header 中的名称
for (const { from, to } of plan.renamed) {
const block = existingBlocks.get(normalizeRequirementName(from));
if (block) {
existingBlocks.delete(normalizeRequirementName(from));
block.name = to;
block.headerLine = `### Requirement: ${to}`;
existingBlocks.set(normalizeRequirementName(to), block);
}
}
// 7. 执行 MODIFIED — 整块替换
for (const mod of plan.modified) {
existingBlocks.set(normalizeRequirementName(mod.name), mod);
}
// 8. 执行 ADDED — 追加新块
for (const add of plan.added) {
existingBlocks.set(normalizeRequirementName(add.name), add);
}
// 9. 重建完整 spec 文件
const rebuilt = reassemble(parts, existingBlocks);
return { rebuilt, counts: { added, modified, removed, renamed } };
}
合并操作示意
主 spec (openspec/specs/auth/spec.md):
## Requirements
### Requirement: Login
### Requirement: Session Timeout (60min)
### Requirement: Remember Me
Delta spec (changes/add-2fa/specs/auth/spec.md):
## ADDED Requirements
### Requirement: Two-Factor Authentication
## MODIFIED Requirements
### Requirement: Session Timeout (改为 30min)
## REMOVED Requirements
### Requirement: Remember Me
合并后的主 spec:
## Requirements
### Requirement: Login ← 保留不变
### Requirement: Session Timeout (30min) ← 被 MODIFIED 替换
### Requirement: Two-Factor Authentication ← ADDED 追加
安全检查
- 重名检测:ADDED 分区内不允许重复名称,也不允许与已有 Requirement 重名
- MODIFIED 完整性:必须包含完整的 Requirement 内容(非差异),因为是整块替换
- 归档前验证:合并后的 spec 会经过 Validator 再次验证格式
验证系统
Validator 类:core/validation/validator.ts
双层验证架构:Zod Schema 结构验证 + 自定义规则语义验证。
export class Validator {
private strictMode: boolean;
constructor(strictMode: boolean = false) {
this.strictMode = strictMode;
}
// Spec 文件验证
async validateSpec(filePath: string): Promise<ValidationReport> {
const content = readFileSync(filePath, 'utf-8');
const parser = new MarkdownParser(content);
const spec = parser.parseSpec(specName);
// 层 1:Zod Schema 结构验证
const result = SpecSchema.safeParse(spec);
if (!result.success) {
issues.push(...this.convertZodErrors(result.error));
}
// 层 2:自定义规则验证
issues.push(...this.applySpecRules(spec, content));
return this.createReport(issues);
}
// Change 文件验证
async validateChange(filePath: string): Promise<ValidationReport> {
// 类似流程,使用 ChangeParser + ChangeSchema
}
// 字符串内容验证(归档前的合并结果验证)
async validateSpecContent(
specName: string, content: string
): Promise<ValidationReport> { ... }
}
验证报告结构
export interface ValidationIssue {
level: 'ERROR' | 'WARNING'; // 严重级别
path: string; // 问题路径
message: string; // 错误描述
}
export interface ValidationReport {
valid: boolean; // 是否通过
issues: ValidationIssue[];
errors: number;
warnings: number;
}
自定义规则示例
- Purpose 长度:spec 的 Purpose 描述不能少于
MIN_PURPOSE_LENGTH字符 - Requirement 文本长度:不能超过
MAX_REQUIREMENT_TEXT_LENGTH - Scenario 格式:必须使用
####(4 个 #),不是 3 个 - RFC 2119 关键字:检查 SHALL/MUST/SHOULD/MAY 的使用
命令生成架构
统一内容模型
命令生成系统的核心思想:一次生成内容,多格式输出。
// core/command-generation/types.ts
export interface CommandContent {
id: string; // 命令 ID,如 "propose"
name: string; // 显示名称
description: string; // 命令描述
category: string; // 分类
tags: string[]; // 标签
body: string; // 完整的指令内容
}
export interface ToolCommandAdapter {
toolId: string;
getFilePath(commandId: string): string; // 输出文件路径
formatFile(content: CommandContent): string; // 格式化为文件内容
}
生成流程
getCommandContents() 获取所有命令内容 (CommandContent[])
│
▼
CommandAdapterRegistry 查找目标工具的适配器
│
▼
adapter.getFilePath(id) 计算输出文件路径
│
▼
adapter.formatFile(content) 按工具格式渲染文件
│
▼
写入文件系统 .claude/commands/opsx/propose.md
.cursor/rules/opsx/propose.md
.github/prompts/opsx-propose.prompt.md
适配器模式详解
Claude Code 适配器源码
// core/command-generation/adapters/claude.ts
// YAML 值转义
function escapeYamlValue(value: string): string {
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
const escaped = value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n');
return `"${escaped}"`;
}
return value;
}
export const claudeAdapter: ToolCommandAdapter = {
toolId: 'claude',
getFilePath(commandId: string): string {
// → .claude/commands/opsx/propose.md
return path.join('.claude', 'commands', 'opsx', `${commandId}.md`);
},
formatFile(content: CommandContent): string {
// Claude Code 的 YAML frontmatter 格式
return `---
name: ${escapeYamlValue(content.name)}
description: ${escapeYamlValue(content.description)}
category: ${escapeYamlValue(content.category)}
tags: ${formatTagsArray(content.tags)}
---
${content.body}
`;
},
};
适配器工厂
// core/command-generation/adapters/factory.ts
// 适配器注册表,按 toolId 查找
const adapters: Map<string, ToolCommandAdapter> = new Map([
['claude', claudeAdapter],
['cursor', cursorAdapter],
['github-copilot', copilotAdapter],
['windsurf', windsurfAdapter],
['gemini', geminiAdapter],
['codex', codexAdapter],
// ... 20+ 更多
]);
export function getAdapter(toolId: string): ToolCommandAdapter | undefined {
return adapters.get(toolId);
}
各适配器输出路径对比
| 工具 | 输出路径 | 格式特点 |
|---|---|---|
| Claude Code | .claude/commands/opsx/<id>.md | YAML frontmatter (name, description, category, tags) |
| Cursor | .cursor/rules/opsx/<id>.md | Cursor 特定 frontmatter |
| GitHub Copilot | .github/prompts/opsx-<id>.prompt.md | Prompt 文件格式 |
| Gemini CLI | .gemini/commands/opsx/<id>.md | Gemini 命令格式 |
| Continue | .continue/config/<id>.json | JSON 配置 |
Skill 生成系统
工作流模板:core/templates/workflows/
每个工作流都有一个 TypeScript 文件,返回结构化的 SkillTemplate:
// core/templates/workflows/propose.ts
export function getOpsxProposeSkillTemplate(): SkillTemplate {
return {
name: 'openspec-propose',
description: 'Propose a new change with all artifacts...',
instructions: `
Propose a new change...
**Steps**
1. Ask what they want to build
2. Create change directory: openspec new change "<name>"
3. Get artifact build order: openspec status --change "<name>" --json
4. Create artifacts in dependency order:
a. Get instructions: openspec instructions <id> --change "<name>" --json
b. Read template, instruction, context, rules
c. Create artifact file
5. Continue until all applyRequires are done
`,
};
}
Profile 系统
// core/profiles.ts
export const CORE_WORKFLOWS = [
'propose', 'apply', 'archive'
];
export const ALL_WORKFLOWS = [
'explore', 'new', 'continue', 'apply', 'ff',
'sync', 'archive', 'bulk-archive', 'verify', 'onboard', 'propose'
];
export function getProfileWorkflows(profile: Profile): string[] {
if (profile === 'core') return CORE_WORKFLOWS;
return ALL_WORKFLOWS;
}
core profile:只生成 propose/apply/archive 三个核心命令。
custom profile:生成全部 11 个工作流命令。
配置系统
双层配置架构
全局配置 (core/global-config.ts)
→ ~/.config/openspec/config.json (XDG 规范)
→ 存储:profile, delivery, workflows, featureFlags
→ 影响:所有项目
项目配置 (core/project-config.ts)
→ openspec/config.yaml
→ 存储:schema, context, rules
→ 影响:当前项目
项目配置解析:core/project-config.ts
export interface ProjectConfig {
schema?: string;
context?: string; // 最大 50KB
rules?: Record<string, string[]>; // artifactId → rules[]
}
export function readProjectConfig(projectRoot: string): ProjectConfig | null {
const configPath = path.join(projectRoot, 'openspec', 'config.yaml');
if (!fs.existsSync(configPath)) return null;
const content = fs.readFileSync(configPath, 'utf-8');
const parsed = yaml.parse(content);
// Zod 验证 + 类型安全
return ProjectConfigSchema.parse(parsed);
}
// 验证 rules 中的 artifact ID 是否存在于当前 schema
export function validateConfigRules(
rules: Record<string, string[]>,
validArtifactIds: Set<string>,
schemaName: string
): string[] {
const warnings: string[] = [];
for (const key of Object.keys(rules)) {
if (!validArtifactIds.has(key)) {
warnings.push(
`Warning: rules key "${key}" is not a valid artifact ID in schema "${schemaName}"`
);
}
}
return warnings;
}
遥测系统
匿名遥测:src/telemetry/
OpenSpec 使用 PostHog 收集匿名使用统计:
- 收集内容:命令名称 + CLI 版本号
- 不收集:命令参数、文件路径、用户数据 (PII)
- 首次运行提示:通过
maybeShowTelemetryNotice()显示一次
// 每个命令执行前自动追踪
program.hook('preAction', async (thisCommand, actionCommand) => {
await maybeShowTelemetryNotice();
const commandPath = getCommandPath(actionCommand);
await trackCommand(commandPath, version);
});
// 命令完成后关闭客户端
program.hook('postAction', async () => {
await shutdown();
});
# 退出遥测
export OPENSPEC_TELEMETRY=0
# 或
export DO_NOT_TRACK=1
设计模式总结
核心设计模式
| 模式 | 应用位置 | 解决的问题 |
|---|---|---|
| 工厂方法 | ArtifactGraph.fromYaml() / fromSchema() | 多种方式构建图实例 |
| 适配器 | command-generation/adapters/* | 统一接口适配 25+ 工具 |
| 策略 | completions/generators/* | 不同 shell 的补全生成 |
| 注册表 | CommandAdapterRegistry | 按 toolId 动态查找适配器 |
| 模板方法 | templates/workflows/* | 统一结构,定制内容 |
| Builder | buildUpdatedSpec() | 逐步构建合并后的 spec |
| 状态机 | artifact-graph/state.ts | done / ready / blocked 三态 |
架构亮点
- 文件即状态:用文件系统代替数据库,零依赖且 Git 友好
- Schema 驱动:schema.yaml 定义工作流,修改 YAML 即改变行为
- 三级覆盖:项目 > 用户 > 内置,满足个性化需求
- 内容与格式分离:CommandContent 统一内容,Adapter 负责格式转换
- 确定性排序:Kahn 算法中双重 sort(),保证相同输入相同输出
- 渐进式采纳:brownfield 优先设计,通过隔离的 change 目录避免侵入
总结
OpenSpec 源码核心技术总结
| 模块 | 核心技术 | 关键文件 |
|---|---|---|
| 依赖图 | Kahn 拓扑排序 | artifact-graph/graph.ts |
| 状态检测 | 文件系统 + fast-glob | artifact-graph/state.ts |
| Schema 解析 | 三级覆盖 + XDG 规范 | artifact-graph/resolver.ts |
| 指令富化 | 多源聚合(schema + config + template) | artifact-graph/instruction-loader.ts |
| Spec 合并 | Delta 操作 (ADDED/MODIFIED/REMOVED) | core/specs-apply.ts |
| 验证 | Zod Schema + 自定义规则 | validation/validator.ts |
| 多工具适配 | 适配器模式 + 注册表 | command-generation/adapters/* |
| Markdown 解析 | 正则 + 行级扫描 | parsers/requirement-blocks.ts |
延伸阅读
- 使用教程:OpenSpec 完全使用教程
- SDD 实操:Claude Code + SDD 实操落地教程
- GitHub 源码:Fission-AI/OpenSpec