Files
HarborForge/docs/proposal-essential-story-migration.md
zhi c2b11248d7 DOC-002: Proposal/Essential/Story restricted migration guide
- Document Propose → Proposal naming changes across all layers
- Document Proposal Accept semantic change (single task → Essential-based batch)
- Document story/* restricted expansion and impact analysis
- Document feat_task_id deprecation and backward compatibility strategy
- Document migration execution order and rollback plan
2026-03-29 14:32:23 +00:00

264 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HarborForge — Proposal / Essential / Story Restricted 迁移说明
> 更新时间2026-03-29
> 本文档描述 Propose → Proposal 重命名、Proposal Accept 语义变更、Essential 新增、以及 story/* restricted 的迁移细节与兼容策略。
---
## 一、Propose → Proposal 命名调整
### 1.1 变更范围
| 层 | 旧命名 | 新命名 | 涉及文件 |
|---|--------|--------|---------|
| **数据库表** | `proposes` | `proposals`(新表名) | Alembic migration |
| **Model** | `Propose`, `ProposeStatus` | `Proposal`, `ProposalStatus` | `models/propose.py``models/proposal.py` |
| **Schema** | `ProposeCreate`, `ProposeUpdate`, `ProposeResponse`, `ProposeStatusEnum`, `ProposeBase` | `ProposalCreate`, `ProposalUpdate`, `ProposalResponse`, `ProposalStatusEnum`, `ProposalBase` | `schemas/schemas.py` |
| **Router** | `proposes.py`, 路由前缀 `/proposes` | `proposals.py`, 路由前缀 `/proposals` | `api/routers/proposes.py``api/routers/proposals.py` |
| **Helper 函数** | `_find_propose()`, `_serialize_propose()`, `_generate_propose_code()`, `_can_edit_propose()` | `_find_proposal()`, `_serialize_proposal()`, `_generate_proposal_code()`, `_can_edit_proposal()` | 同上 router 文件 |
| **Code 字段** | `propose_code` | `proposal_code` | Model、Schema、序列化逻辑 |
| **Activity log** | `entity_type="propose"` | `entity_type="proposal"` | `log_activity()` 调用处 |
| **权限 key** | `propose.accept`, `propose.reject`, `propose.reopen` | `proposal.accept`, `proposal.reject`, `proposal.reopen` | `check_permission()` 调用处、RBAC 数据 |
| **前端** | 页面标题、菜单、API 调用中的 `propose` 文案 | 统一为 `proposal` | 前端组件、路由、API 调用 |
| **CLI** | `hf propose`, `hf propose-create`, `hf propose-accept` 等 | `hf proposal`, `hf proposal create`, `hf proposal accept` 等 | CLI 命令注册与 help 文案 |
### 1.2 数据库迁移策略
```sql
-- Alembic migration
ALTER TABLE proposes RENAME TO proposals;
ALTER TABLE proposals CHANGE propose_code proposal_code VARCHAR(64);
```
- `proposal_code` 的已有编码值(如 `HF:P00001`)不变,仅字段名变更
- 所有外键引用(如 `proposals.id`)保持不变(表内 PK 不变)
### 1.3 API 兼容策略
**方案:直接切换,不保留旧路由**
理由:
- HarborForge 目前为内部系统,无外部消费者
- CLI 和前端同步更新,不存在旧客户端兼容问题
- 保留兼容别名会增加长期维护负担
具体做法:
1. 后端路由从 `/projects/{pid}/proposes` 切换为 `/projects/{pid}/proposals`
2. 前端所有 API 调用同步更新
3. CLI 命令同步更新
4. **不保留** `/proposes` 旧路由
> 如果未来需要面向外部提供 API应在 API 版本化层面处理兼容,而非在路由别名层面。
---
## 二、Proposal Accept 语义变化
### 2.1 旧行为
```
Proposal (open) ──accept──▶ Proposal (accepted)
单个 story/feature Task
feat_task_id 回填到 Proposal
```
- Accept 时自动创建 **1 个** `story/feature` task
- `feat_task_id` 字段记录生成的 task ID
### 2.2 新行为
```
Proposal (open)
├── Essential (feature) ──┐
├── Essential (improvement) ─┤── accept ──▶ Proposal (accepted)
└── Essential (refactor) ──┘ │
story/feature Task ← from Essential(feature)
story/improvement Task ← from Essential(improvement)
story/refactor Task ← from Essential(refactor)
全部落入选定 Milestone
```
- Proposal 下维护多个 **Essential**(可落地核心条目)
- Accept 时,遍历该 Proposal 下全部 Essential
- 按 Essential.type 映射生成对应 `story/*` task
- `feature``story/feature`
- `improvement``story/improvement`
- `refactor``story/refactor`
- 生成目标 Milestone 在 Accept 时明确选择(与旧行为一致)
- **批量创建在同一个数据库事务中完成**
### 2.3 Accept 前置条件变更
| 条件 | 旧 | 新 |
|------|----|----|
| 操作者权限 | `propose.accept` | `proposal.accept` |
| 目标 Milestone | 必须选择,状态 `open` | 同上 |
| Essential 数量 | 无此概念 | **必须至少 1 个 Essential**,否则 Accept 报错 |
### 2.4 Accept 返回值变更
- 旧:返回 Proposal 详情(含 `feat_task_id`
- 新:返回 Proposal 详情 + 嵌套的 Essential 列表 + 每个 Essential 生成的 task 信息
---
## 三、story/* 改为 Restricted 的影响面
### 3.1 当前状态
后端 `tasks.py` 中已有 restricted 机制:
```python
RESTRICTED_TYPE_SUBTYPES = {
("story", "feature"),
}
```
目前仅 `story/feature` 是 restricted。
### 3.2 变更内容
**所有 `story/*` 组合** 标记为 restricted
```python
RESTRICTED_TYPE_SUBTYPES = {
("story", "feature"),
("story", "improvement"),
("story", "refactor"),
}
```
### 3.3 影响面
| 入口 | 影响 | 处理 |
|------|------|------|
| **通用 Task Create API** (`POST /projects/{pid}/milestones/{mid}/tasks`) | 无法创建任何 `story/*` task | 已有 `_validate_task_type_subtype()` 校验,扩展 restricted 集合即可 |
| **Proposal Accept** | 内部受控入口,`allow_restricted=True` | 保留,唯一合法创建 `story/*` 的途径 |
| **前端 Task 创建表单** | `story` 类型从选择器中移除或标为不可选 | 前端调整 |
| **CLI `hf task create`** | 阻止 `--type story` | CLI 侧校验或依赖后端 403 |
| **CSV 导入**(如有) | 阻止 `story/*` 行 | 导入逻辑需加校验 |
| **Milestone 下 Task Create** (`milestones.py` 路由) | 已有 `story/feature` 限制逻辑 | 扩展为全部 `story/*` |
### 3.4 已有数据兼容
- 历史上通过旧 Accept 创建的 `story/feature` task **保持不变**
- 不需要迁移或修改已有 story task
- restricted 仅影响 **新建**,不影响已有记录的读取/更新/状态流转
---
## 四、旧数据与旧接口兼容策略
### 4.1 `feat_task_id` 字段
| 处理方式 | 说明 |
|---------|------|
| **保留字段,标记为 deprecated** | 数据库列保留,避免破坏已有数据 |
| **只读** | API 继续返回 `feat_task_id`(已有 Proposal 的值不丢失) |
| **新 Proposal 不再写入** | 新的 Accept 流程不再设置 `feat_task_id` |
| **新增追踪方式** | 通过 Essential → Task 的关联关系(`essential_id` 或关联表)替代 |
| **未来清理** | 等所有旧 Proposal 迁移完毕后,可选择 DROP 该列 |
### 4.2 旧 Proposal 数据读取
- 旧的 `accepted` 状态 Proposal 仍有 `feat_task_id`,前端继续支持 "View Generated Task" 跳转
- 旧 Proposal 没有 Essential详情页 Essential 区域显示空列表
- 不需要为旧 Proposal 补建 Essential 记录
### 4.3 Proposal Code 格式
- 已有编码如 `HF:P00001` 保持不变
- 新 Proposal 继续使用相同格式
- 字段名从 `propose_code` 变为 `proposal_code`,但值的格式不变
### 4.4 权限 Key 迁移
| 旧 | 新 | 迁移方式 |
|----|----|----|
| `propose.accept` | `proposal.accept` | 数据库中更新 RBAC permission 记录 |
| `propose.reject` | `proposal.reject` | 同上 |
| `propose.reopen` | `proposal.reopen` | 同上 |
```sql
-- RBAC permission key migration
UPDATE permissions SET key = REPLACE(key, 'propose.', 'proposal.') WHERE key LIKE 'propose.%';
```
> 如果权限 key 存储在 code 中而非数据库,则只需修改代码常量。
### 4.5 Activity Log 兼容
- 旧 activity log 中 `entity_type="propose"` 的记录保留不动
- 前端展示时做兼容映射:`propose` → 显示为 "Proposal"
- 新 activity log 统一使用 `entity_type="proposal"`
---
## 五、Essential 新增概要
### 5.1 数据模型
```
Essential
├── id (PK)
├── essential_code (UNIQUE) — 格式参考 ProjectCode / TaskCode
├── proposal_id (FK → proposals.id)
├── type (Enum: feature, improvement, refactor)
├── title
├── description (nullable)
├── created_by_id (FK → users.id, nullable)
├── created_at
└── updated_at
```
### 5.2 EssentialCode 编码
- 格式:`{proposal_code}:E{i:05x}`
- 示例:`HF:P00001:E00001`
- 每个 Proposal 独立递增
### 5.3 Essential 生命周期
1. Proposal `open` 状态下可创建/编辑/删除 Essential
2. Proposal Accept 时,全部 Essential 用于生成 `story/*` task
3. Proposal 进入 `accepted`/`rejected`Essential 不可修改
### 5.4 API 端点
| 操作 | 方法 | 路径 |
|------|------|------|
| 列出 | GET | `/projects/{pid}/proposals/{proposal_id}/essentials` |
| 创建 | POST | `/projects/{pid}/proposals/{proposal_id}/essentials` |
| 详情 | GET | `/projects/{pid}/proposals/{proposal_id}/essentials/{id}` |
| 编辑 | PATCH | `/projects/{pid}/proposals/{proposal_id}/essentials/{id}` |
| 删除 | DELETE | `/projects/{pid}/proposals/{proposal_id}/essentials/{id}` |
---
## 六、迁移执行顺序
推荐按以下顺序执行,确保每一步可独立验证:
1. **后端 Model/Schema 重命名**`Propose``Proposal`BE-PR-001, BE-PR-002
2. **数据库 migration** — 表名、字段名变更
3. **新增 Essential Model/Schema**BE-PR-003, BE-PR-004, BE-PR-005
4. **新增 Essential CRUD API**BE-PR-006
5. **重构 Accept 逻辑**BE-PR-007, BE-PR-008
6. **收紧 story restricted**BE-PR-009
7. **清理 feat_task_id**BE-PR-010
8. **前端同步更新**FE-PR-001 ~ FE-PR-005
9. **CLI 同步更新**CLI-PR-001 ~ CLI-PR-004
10. **测试补全**BE-PR-011, TEST-BE-PR-001, TEST-FE-PR-001, TEST-CLI-PR-001
---
## 七、风险与注意事项
1. **表重命名的 downtime**`ALTER TABLE RENAME` 在 MySQL 中是瞬时操作,但需确保无长事务锁表
2. **权限 key 更新** — 如果有缓存层,需要在迁移后清理缓存
3. **前后端同步部署** — 建议同时部署前后端,避免前端调旧路由 404
4. **回滚方案** — 如需回滚反向执行表重命名和字段重命名即可Essential 表作为新增表,回滚时 DROP 即可