以"油价监控历史趋势图支持多油号"需求为例,记录在项目已有历史文档的情况下,OpenSpec 如何利用现有档案完成迭代开发。
背景
项目已通过 OpenSpec 完成过一次完整开发:
bash
归档记录:
openspec/changes/archive/2026-08-24-oil-price-monitor/
├── proposal.md
├── specs/oil-price-monitor/spec.md ← 主规格文档(已存在)
├── design.md
└── tasks.md
现有系统已上线运行,用户提出新需求:历史趋势图从单选油号改为多选油号。
这是一个增量变更(迭代开发),而非从零开始的新项目。
1. Explore 阶段:借助历史代码理解现状
做了什么
用户提出需求后,进入 Explore 阶段纯粹讨论方案。
没有创建任何新文件。
关键动作是阅读现有代码,找到需要修改的位置:
scss
┌─────────────────────────────────────────────┐
│ Explore 阶段 │
│ - 读取 web/templates/index.html │
│ - 找到 updateChart() 函数 │
│ - 确认当前是单选油号下拉框实现 │
│ - 确定多选改造方案 │
│ - 不生成任何文件 │
└─────────────────────────────────────────────┘
历史档案利用方式:
本次 Explore 阶段没有显式读取 OpenSpec 档案(因为是在已有代码库上做变更,代码本身就是档案),但 AI 通过阅读代码理解了:
- 当前的图表实现方式
- 城市筛选已有复选框交互可参考
- 油号选择器需要改造的位置
关键输出
确认了改造方案:
- 自定义 checkbox 下拉展开式(参考城市筛选的交互)
- 默认全选所有油号
- 城市用颜色区分,油号用线型区分
2. Propose 阶段:声明变更范围,创建增量规格
创建了新变更目录
arduino
openspec new change "oil-price-multi-oiltype-chart"
bash
openspec/changes/oil-price-multi-oiltype-chart/ ← 新建
├── .openspec.yaml
├── proposal.md ← 新建
├── specs/
│ └── oil-price-monitor/
│ └── spec.md ← 新建(delta spec)
├── design.md ← 新建
└── tasks.md ← 新建
关键:读取历史主规格文档
在创建 delta spec 之前,AI 读取了归档中的历史主规格文档:
bash
归档路径:
openspec/changes/archive/2026-08-24-oil-price-monitor/
└── specs/oil-price-monitor/spec.md ← 被读取
历史规格中,"历史油价趋势图表"需求原文:
shell
### Requirement: 历史油价趋势图表
系统 SHALL 提供 ECharts 折线图展示历史油价趋势。
图表 SHALL 支持多城市叠加显示,用户可勾选显示哪些城市。
图表 X 轴为日期,Y 轴为价格。
用户 SHALL 可选择切换不同油号查看。
这是创建 delta spec 的依据------AI 看到了"单选油号"的原始描述,才能写出"改为多选"的新描述。
proposal.md 中声明变更类型
markdown
## Capabilities
### New Capabilities
- (无)
### Modified Capabilities
- `oil-price-monitor`: 修改"历史油价趋势图表"规格,
将油号选择从单选改为多选,支持同时展示多个油号趋势
这里的关键是:oil-price-monitor 是已存在的 capability ,不是新创建的。OpenSpec 据此在 specs/oil-price-monitor/ 下创建了 delta spec。
delta spec 只写变化的部分
specs/oil-price-monitor/spec.md(delta)的内容:
erlang
## MODIFIED Requirements
### Requirement: 历史油价趋势图表
**原内容:**
系统 SHALL 提供 ECharts 折线图展示历史油价趋势。图表 SHALL 支持多城市叠加显示...
用户 SHALL 可选择切换不同油号查看。
**新内容:**
系统 SHALL 提供 ECharts 折线图展示历史油价趋势,支持多城市与多油号同时展示。
油号选择器 SHALL 为多选下拉控件,初始默认全选所有6个油号...
图表每条线 SHALL 以"城市-油号"格式命名(如"北京-92#")。
城市 SHALL 用颜色区分,油号 SHALL 用线型区分...
没有重复历史文档中未变化的内容,只写了:
- 引用原内容(便于对比)
- 新内容(描述变更后的行为)
design.md 比首次开发更轻
因为架构已经确定(Flask + Bootstrap + ECharts),design.md 只需聚焦增量部分:
shell
## Context
当前历史趋势图使用单选油号下拉框... 需求变更为多选油号。
## Decisions
### 1. 油号多选 UI 实现
自定义 checkbox 下拉展开式(参考城市筛选交互)
### 2. series 生成逻辑
从单油号遍历改为 (城市 × 油号) 双遍历
### 3. 线型映射
p92→solid, p95→dashed, ...
对比首次开发的 design.md:省去了技术栈选型、目录结构设计、API 设计等已在历史文档中确定的内容。
tasks.md 只列出增量任务
ini
## 1. 前端 UI 改造
- [ ] 1.1 将油号下拉框替换为自定义 checkbox 下拉控件
- [ ] 1.2 实现下拉展开时显示6个油号checkbox,默认全部勾选
## 2. 图表数据逻辑
- [ ] 2.1 修改 selectedOilTypes 从单值改为数组
- [ ] 2.2 修改 updateChart() 的 series 生成逻辑
...
对比首次开发的 29 个任务,这次只有 11 个任务------因为后端代码完全不需要改动。
3. Apply 阶段:仅改动一行文件
实现前的状态
历史归档中已有完整实现代码:
arduino
oil-price-monitor/
├── config/
├── data/
├── scripts/
│ └── fetch_price.py
├── web/
│ ├── app.py
│ └── templates/
│ └── index.html ← 只需修改这个文件
├── requirements.txt
└── README.md
实现后的改动
只修改了一个文件:
bash
web/templates/index.html ← 唯一被修改的文件
具体改动包括:
-
CSS:新增
.oil-multiselect等样式(约60行) -
HTML:将
<select>替换为自定义 checkbox 下拉控件 -
JS:
- 新增
selectedOilTypesSet(替代原单值) - 新增
initOilTypes/toggleOilMultiselect/toggleOilType/updateOilTagsDisplay - 修改
updateChart()的 series 生成逻辑 - 修改 ECharts 配置
- 新增
未改动:
web/app.py--- API 无变化scripts/fetch_price.py--- 数据获取逻辑无变化config/--- 配置无变化
tasks.md 逐项标记完成
每完成一个任务就把 - [ ] 改为 - [x]:
scss
## 2. 图表数据逻辑
- [x] 2.1 修改 selectedOilTypes 状态管理,从单值改为数组
- [x] 2.2 修改 updateChart() 的 series 生成逻辑
- [x] 2.3 修改 series.name 格式为"城市-油号"
4. Archive 阶段:归档变更档案
执行归档
bash
mv openspec/changes/oil-price-multi-oiltype-chart \
openspec/changes/archive/2026-08-24-oil-price-multi-oiltype-chart
归档后目录结构
bash
openspec/changes/archive/
├── 2026-08-24-oil-price-monitor/ ← 历史首次开发档案
│ ├── proposal.md
│ ├── specs/oil-price-monitor/spec.md
│ ├── design.md
│ └── tasks.md
│
└── 2026-08-24-oil-price-multi-oiltype-chart/ ← 新迭代档案
├── proposal.md
├── specs/oil-price-monitor/spec.md ← delta spec
├── design.md
└── tasks.md
两个档案并列存在,各自记录一次独立的变更。
5. 全流程文件变动总览
| 阶段 | 新建文件 | 修改文件 | 读取的历史档案 |
|---|---|---|---|
| Explore | --- | --- | web/templates/index.html(代码本身) |
| Propose | proposal.md specs/oil-price-monitor/spec.md design.md tasks.md .openspec.yaml |
--- | archive/.../specs/oil-price-monitor/spec.md |
| Apply | --- | web/templates/index.html |
tasks.md(逐项完成) |
| Archive | --- | --- | --- |
总新建文件数:5个(.openspec.yaml + 4个artifacts)
总修改代码文件:1个
6. 关键心得:OpenSpec 增量变更的设计
1. 增量变更的核心:delta spec
markdown
历史主规格(不可修改)
│
│ ← 读取后创建 delta
▼
delta spec(记录变化)
│
│ ← 归档时可选同步到主规格
▼
主规格更新(可选)
Delta spec 的价值:不修改历史档案,只记录增量变化。即使归档后,也能看到每次变更的内容和原因。
2. proposal 是变更的入口
proposal.md 中的 Modified Capabilities 声明了哪个已存在的 capability 被修改。这决定了:
- delta spec 放在哪个路径下(
specs/oil-price-monitor/) - OpenSpec 知道这不是新能力,而是对
oil-price-monitor的修改
3. 任务量随存量递减
首次开发:29个任务(全部新建)
第二次迭代:11个任务(只改 UI + 图表逻辑)
第三次迭代:(如果还有)可能只有3-4个任务
存量越大,增量越小。OpenSpec 的任务清单只记录"这一次需要做什么",而非重复历史已做的工作。
4. Archive 不合并,只归档
两次迭代的变更档案并列保存,不合并:
- 第一次:
archive/2026-08-24-oil-price-monitor/ - 第二次:
archive/2026-08-24-oil-price-multi-oiltype-chart/
这样做的好处是:可以追溯每次决策的背景和上下文。即使未来主规格被更新,历史 delta 仍然记录了当时的思考过程。
5. 实现代码和规划文档分离
bash
规划文档 → openspec/changes/archive/yyyymmdd-xxx/
实现代码 → oil-price-monitor/
Archive 移动的是 openspec 下的规划文档,不是项目实现代码。两者独立存在,实现代码不受 OpenSpec 工作流的影响。