# 关键设计决策记录

每条决策都包含**为什么这么做**，避免后续模型重新讨论或推翻。

## 1. 进度计算 — 由状态推导，不要手填百分比

**问题**：Jira/TAPD 等系统让开发手填进度（20%/35%/50%），数据完全主观，不可靠。

**决策**：DevTask 进度由 `status` 推导：
- `todo` → 0%
- `in_progress` → 50%
- `testing` → 80%
- `submitted` → 100%

**理由**：客观、不允许造假。需求级/版本级进度 = `Σ(估算耗时 × 状态推导%) / Σ(估算耗时)`。估算耗时优先取执行人填写的 `estimateHours`，没有时才用 AI 草案的 `aiEstimateHours` 兜底。

## 2. 实际工时 — 由开始/完成时间戳计算

**问题**：让人手填工时太粗糙（一天 8h 真的全部花在这个任务吗？）。

**决策**：精确到 0.5h 的时间差计算
```
实际工时 = (completedAt - startDate) / 3600000，精确到 0.5h
```

**理由**：状态流转时自动记录精确时间戳，工时直接派生。

## 3. DevTask "已提测" = 终态

**问题**：原本有 `submitted → done`，但开发提测之后就交付完成了，后续测试结果不应该回过来改 DevTask 状态。

**决策**：去掉 `done`，`submitted` 是终态。测试通过/失败产生 Bug，不影响 DevTask。

**理由**：DevTask 的责任是"开发到提测"，验收是 TestCase 的职责。

## 4. 调研/产品/UI — 自动开始 + 手动提前开始

**问题**：到了计划开始日期，状态还是 pending 不合理。

**决策**：
- 当前时间 ≥ `startTime` 时自动视为 in_progress
- 用户可在计划日期前手动点"开始"
- 自动开始时记录 `actualStartAt`（精确到分钟）

**理由**：减少冗余操作但保留灵活性。

## 5. TestCase 主归属版本，需求是可选标签

**问题**：早期把测试用例挂在需求上，但同一需求跨版本迭代时用例难以管理。

**决策**：
- TestCase.versionId 必填（执行归属）
- TestCase.requirementId 可选（语义标签）
- 测试人员从"我的测试用例"看到的就是当前版本的用例

**理由**：测试是按版本执行的，不是按需求；但需求作为思考维度仍然有用。

## 6. Bug 直接挂版本

**问题**：早期通过 `Bug → TestCase → Requirement → Version` 四级反查，太绕。

**决策**：Bug 增加 `versionId` 字段，直接挂版本。`testCaseId` 保留作为来源追溯。

**理由**：判断"能不能发布"的唯一维度是版本，必须 O(1) 查询。

## 7. 跨模块通信 — 引擎层，不是事件总线

**问题**：模块多了，A store 改动影响 B 的派生数据，散落各处难维护。

**决策**：建立**纯函数引擎**：
- `linkage-engine.ts`：从 DevTask 状态派生需求"实际进度"
- `workspace-engine.ts`：聚合所有 store 数据为统一 WorkItem

**理由**：事件总线过度设计，纯函数派生更可预测。新增模块只需在引擎中添加聚合逻辑。

## 8. 时间字段统一 ISO 时间戳到分钟

**问题**：日期(`YYYY-MM-DD`) vs 时间戳混用，UI 展示不一致。

**决策**：所有"实际开始/完成"用 `new Date().toISOString()`，展示统一 `slice(0, 16).replace('T', ' ')`。

**理由**：时间戳到分钟够精确，不像秒/毫秒那么噪。

## 9. 任务类型字典化 + 分组

**问题**：早期 enum 写死类型（前端/后端/大数据/其他），新增类型需改代码。

**决策**：建 TaskCategory 字典表 + group（development/implementation/other），管理员可增删。

**理由**：业务变化快，配置 > 写死。group 方便统计（开发工时 vs 实施工时）。

## 10. 阻塞正交标记，不是状态值

**问题**：`status: blocked` 会丢信息——`开发中+阻塞` ≠ `自测+阻塞`。

**决策**：
- `status: todo|in_progress|testing|submitted`
- `isBlocked: boolean` + `blockReason: string` + `blockedById: string`
- DevTask 存在阻塞时不能转为 `submitted`，必须先解除阻塞再提测。

**理由**：阻塞和状态正交。工作台筛选 `isBlocked=true` 一键拉出所有阻塞项，跨状态；但 `submitted` 代表开发交付完成，仍必须满足“当前无阻塞”的完成条件。

## 11. 加班原因可选，且去掉"其他"

**问题**：可选"其他"会让人逃避思考具体原因。

**决策**：变更原因/加班原因严格定义有限选项，**没有"其他"**。

**理由**：强制具体化，统计才有意义。

## 12. 项目维度 vs 个人维度耗时分开计算

**问题**：多个开发并行做任务，简单加总会重复计算时间。

**决策**：
- **个人耗时**：每个任务独立累加（`Σ(end - start)`）
- **项目维度阶段耗时**：`min(start)` → `max(end)` 日历跨度（不重复）

**理由**：两个维度回答不同问题——"谁花了多少时间" vs "这个阶段拖了多久"。

## 13. 删除版本时清理孤儿数据

**问题**：删版本只删 version 实体，留下 PlanTask/DevTask/TestCase/Bug 变成孤儿数据。

**决策**：删除版本时级联清理：
1. 释放需求 versionId（回需求池）
2. 删除 PlanTask
3. 删除 DevTask（通过需求 ID）
4. 删除 TestCase
5. 删除 Bug

**理由**：localStorage 没外键约束，必须手动级联。

## 14. 工作台数据点击 → 侧边详情 + 可操作

**问题**：早期工作台只显示信息，操作要跳转到版本详情。

**决策**：点击卡片打开 Drawer（复用版本详情用的同一组件），可在 Drawer 内完成状态流转、转交、提 Bug 等。

**理由**：减少跳转，工作台一站式处理。Drawer 顶部显示"产品/项目/版本"上下文，不会迷失。

## 15. UI 阴影统一 shadow-2xl

**问题**：各 Drawer 阴影不一致（有些 `shadow-lg` 有些 `shadow-2xl`）。

**决策**：全局 Drawer 用 `shadow-2xl`。

**理由**：视觉权重明显，且一致。

## 16. 不进入 Brainstorm 模式的判定

**约定**：以下情况直接修复，**不强制走 brainstorming 流程**：
1. Bug fix（明确 bug，方案直接）
2. 已批准方案的 in-progress 延续
3. 用户已通过 AskUserQuestion 选择了方案
4. 简单文案/样式调整

**理由**：brainstorming 适合新功能设计，bug fix 强行套流程浪费时间。

## 17. AI 拆解不存历史原型，靠对账报告兜底

**问题**：FTB 接入时，禅道里项目可能已经迭代到 V1.6，前面历史原型不在系统里。AI 拆解 V1.6 时如果做"V1.5 → V1.6 diff"，需要用户额外提供历史原型 URL。

**决策**：**不要求用户提供历史原型 URL**。AI 拆解只用三样输入：当前版本原型 + 关联需求 + 版本成员。

**对应风险**：AI 可能把"上版本就有的功能"也当成"本次新做"。

**应对**：要求 Agent 输出**对账报告**，结构化呈现：
- ✅ 完美对应（需求 ↔ 原型注释 ↔ 任务）
- ⚠️ 需求未见原型
- 无需求ID分组（原型有明确功能但没有匹配到关联需求）
- ❓ 注释含糊无法转化

**理由**：让历史版本入库的成本远高于让用户对账的成本。对账报告本来就是 AI 拆解的标配，能同时兜住"拆偏"和"拆漏"两类问题。让团队补需求清单，比让团队补历史原型容易。

## 18. AI 写入必须带 aiDraft 标记 + 引用来源

**问题**：AI 直接写入开发任务/用例，团队无法分辨"这是 AI 拆的还是人手填的"，出错时找不到来源。

**决策**：
- DevTask / TestCase 加 `aiDraft: boolean` 字段
- AI 写入时强制 `aiDraft: true`
- 列表中 AI 草案视觉区分（紫色左边线 + AI 草案徽章）
- 用户编辑保存后 `aiDraft` 自动清除（在 store 的 update 方法里实现）
- 同时加 `references: Reference[]` 字段，记录引用来源（需求 / 原型批注）

**理由**：
- 可识别性：团队一眼分辨 AI 产出 vs 人工产出
- 可追溯性：每条任务知道是从哪条需求 + 哪条原型批注来的
- 可信任：用户编辑即认可，自动转正常状态

## 19. 引用机制对 AI 和人工一致

**问题**：如果只有 AI 任务带 references，人工任务不带，会出现两套规则：用户看到 AI 任务有引用、人工没有，体验割裂；做"任务可追溯"功能时数据缺失。

**决策**：人工创建 DevTask / TestCase 时也强制带 references。
- 表单加「关联需求」（从版本已加需求里多选，自动转 requirement 类型 reference）
- 表单加「原型批注」（手填编号或自由文本，逗号/空格分隔，自动转 prototype_note 类型 reference）
- AI 草案唯一区别只剩 `aiDraft: true` 这一个状态字段

**理由**：单一数据形状，UI 复用；可追溯性是任务本身的属性，不该由"谁创建"决定。

## 20. AI 配置走页面不走 .env

**问题**：早期 ANTHROPIC_API_KEY 只能写到 `apps/server/.env`，配置流程对非技术人员不友好；切换 key 要重启 NestJS。

**决策**：
- 加 `apps/server/src/modules/config/` 模块，提供 GET/PATCH/POST `/api/v1/config/ai` 接口
- 配置存到 `apps/server/data/ai-config.json`（在 .gitignore 里）
- 前端 `/admin/ai-config` 页面（仅超管可见）支持修改 Key、切换模型、测试连接
- AiGateway 优先读文件，其次读环境变量；切换 key 后客户端单例自动重建
- 前端 GET 接口永远不返回完整 Key，只返回 mask（如 `sk-ant...xxxx`）

**理由**：
- 运营/管理可以自助维护，不依赖工程师改代码
- Key 在服务器存储，前端永远拿不到完整内容，安全等同 .env
- 走 .env 的兜底保留，给 CI / 早期部署留路径

## 21. 多 AI 提供商抽象，支持中转站

**问题**：早期 AiGateway 硬绑定 Anthropic 官方 SDK，无法走 ikuncode 等中转站，也不能切换到 OpenAI 格式提供商。

**决策**：引入 Provider 抽象层。
- `apps/server/src/modules/ai/providers/` 下两个实现：`AnthropicProvider` / `OpenAIProvider`
- 共同实现 `AiProvider` 接口（`callTool` / `ping`），把厂商差异封装在 provider 内部
- `prompts/decompose.ts` 用格式无关的 JSON Schema，由各 provider 包装成 Anthropic 的 `input_schema` 或 OpenAI 的 `parameters`
- 配置数据结构升级为 `{ providers: [], activeProviderId }`，支持配置多个 provider 运行时切换
- 旧的单 key 配置自动迁移成 `providers[0]`

**支持范围**：当前两类格式覆盖 99% 中转站
- `anthropic`：Anthropic 官方 / 中转站（兼容 Messages API + tool_use）
- `openai`：OpenAI 官方 / 中转站（兼容 Chat Completions + tool calling）

**理由**：
- 用户可以自由选择厂家或中转站，不被一家锁死
- 每个 provider 独立 model 字段，因为不同提供商上同名模型不一定可用
- "激活"概念让多套配置同时存在，方便 A/B 切换
- 抽象层让上层 AiService 完全不感知厂商差异，未来加 Risk Watch / Schedule Suggest Agent 时复用

## 22. 业务数据先迁到 PostgreSQL AppData，再逐步关系化

**问题**：V1 业务数据存在浏览器 localStorage 中，用户在 Application Storage 执行 Clear site data 后，产品、项目、版本、需求池、版本详情里的调研/产品方案/UI/开发任务/测试用例/Bug，以及成员、任务类型等都会丢失。系统未来要部署到服务器，浏览器不能作为业务数据主存储。

**决策**：先引入服务端通用数据文档层：
- Prisma 增加 `AppData` 模型（表 `app_data`，`key` + JSONB `value`）
- NestJS 增加 `DataModule`，通过 `GET/PUT /api/v1/data/:key` 读写允许的业务数据 key
- 前端 store 保持现有数据形状和业务逻辑，只把持久化从 localStorage 切换到 `/data/:key`
- 覆盖 key：`products-overview`、`requirements`、`version-plans`、`dev-tasks`、`test-cases`、`bugs`、`members`、`task-categories`、`task-worklogs`、`overtime`
- 浏览器只保留登录态，不保存业务主数据

**理由**：
- 当前模块多、数据形状仍在快速调整，一次性全量关系化风险高、改动面大
- JSONB 文档表能立刻解决“清浏览器数据导致业务数据丢失”的线上部署问题
- 保持 store 数据形状不变，能降低迁移对现有 UI、AI 拆解、工作台聚合的冲击
- 后续等字段稳定后，再把 `app_data` 中的文档逐步迁入关系表和领域 CRUD API

## 23. 版本计划完成条件进入工作流引擎，任务类型使用稳定语义码

**问题**：版本模块里，调研/产品方案/UI 设计的完成动作散在 `PlanTab` 和 `PlanDetailDrawer`，右上角勾选按钮会绕过子任务和成果提交。开发任务的任务类型太粗，测试用例没有任务类型，AI 拆解只输出 `role`，无法稳定映射到业务分类。

**决策**：
- 新增 `version-plan-workflow.ts`，统一判断计划是否可勾选子任务、是否可提交成果、是否可完成、缺少什么条件。
- 取消计划卡片右上角的“直接完成”勾选按钮，完成只能通过提交成果触发。
- 调研/产品方案/UI 设计都使用子任务清单；完成时必须满足规则引擎要求并提交链接或文件成果。
- 新增 `requirement-selector.ts`，所有关联需求候选都从当前版本所属项目下的已采纳需求中取，不从全量需求池取。
- DevTask 和 TestCase 共用 `TaskCategory` 字典，`TestCase` 增加 `categoryId`。
- `TaskCategory` 增加稳定 `code`，早期用于 AI `categoryCode` 到系统 `categoryId` 的映射；后续 AI 任务类型改为以 `taskTypeName` 为主，见决策 #41。

**理由**：
- 完成条件属于业务规则，不属于按钮组件；集中到工作流引擎后，列表和抽屉能保持一致。
- 需求候选来源属于领域边界；统一 selector 能避免从需求池、项目需求、版本需求之间误取数据。
- AI 不应该猜数据库 id；稳定语义码能兼容管理员调整展示名称，也方便后续扩展更多模型。
- 测试用例带任务类型后，才能知道测试覆盖范围，AI 拆解和人工创建的数据形状也一致。

## 24. AI 预估和执行人预估分离

**问题**：AI 拆解写入 `estimateHours` 会让系统误以为负责人已经确认了工时和排期；同时 AI 会根据原型自动反推预计开始/截止，造成“计划已填”的错觉。

**决策**：
- AI 输出字段统一改为 `aiEstimateHours`，不再输出/写入 `estimateHours`。
- `estimateHours` 只表示执行人或负责人确认后的执行预估。
- 进度和统计口径优先使用 `estimateHours`；缺失时使用 `aiEstimateHours` 兜底；两者都没有时才从计划时间推导或按无估时处理。
- AI 采纳开发任务时不写预计开始/截止时间，由负责人后续填写计划排期。
- AI 重新拆解入口拆成“重新拆解开发任务”和“重新拆解测试用例”，请求通过 `target` 控制本次只生成哪类草案。
- AI 估时允许小于 0.5h，汇总时保留小数精度；实际耗时仍按状态时间戳计算并保持 0.5h 口径。

**理由**：
- AI 给的是建议，不是负责人承诺。
- 排期属于执行人计划，不能由拆解 Agent 代填。
- 独立重拆能避免修改开发任务时顺手覆盖测试用例，降低误采纳风险。

## 25. AI 重新拆解先过滤已采纳重复草案

**问题**：第一次 AI 拆解的 DevTask/TestCase 已经被采纳后，用户再次重新拆解时，模型可能再次输出同一批草案。若直接展示给用户采纳，会造成开发任务和测试用例重复。

**决策**：
- AI 返回结果后，前端在打开采纳弹窗前先做确定性去重。
- 开发任务比对范围是当前版本已关联需求下的现有 DevTask；测试用例比对范围是当前版本下的现有 TestCase。
- 重复判定使用：任务类型 code + 标题规范化 + 引用来源（需求 / 原型批注）一致。
- 重复草案不进入采纳列表，只在弹窗提示已过滤数量。

**理由**：
- 去重不能只依赖模型提示，必须有系统规则兜底。
- 标题也参与签名，避免同一需求/QY 下不同真实工作项被误过滤。

## 26. 系统内置超级管理员账号不可删除

**问题**：全新系统需要一个稳定可登录的超级管理员账号。早期默认成员曾以普通姓名展示，用户手动改名后，登录态和 Bug 单据中的人员姓名可能仍保留旧快照，造成“提交人是旧姓名、修复人是新姓名”的错位。

**决策**：
- 固定 `m-8` 为系统内置超级管理员账号，姓名、部门、角色由系统保护，不允许删除。
- 读取历史成员数据时自动迁移旧默认账号为“超级管理员”，保留手机号、邮箱、密码。
- 登录态以成员 `id` 为稳定身份，页面启动和编辑当前用户后都按成员表刷新姓名、角色和联系方式。
- Bug 展示和筛选对历史旧名做兼容解析，旧单据中的默认账号旧姓名显示为当前超级管理员。

**理由**：
- 账号身份不能依赖可编辑姓名；姓名只是展示字段。
- 内置超管账号避免新系统初始化后被误删或降权导致无法管理。
- 历史单据兼容能修复已有数据的显示错位，同时不需要批量改写业务记录。

## 27. 测试轮次以第一轮用例为母版

**问题**：同一版本测试失败或修复后，经常需要重新跑一遍同一批测试用例。如果直接复用原用例，会覆盖第一轮执行记录；如果手工重建，又容易漏掉 AI 生成或人工补充的用例。

**决策**：
- TestCase 增加 `roundNo`，旧数据和未显式写入的数据统一视为第 1 轮。
- 点击“开启新一轮测试”时，只从第 1 轮复制用例到下一轮，AI 创建和人工创建的用例都复制。
- 只有最新一轮测试用例全部进入 `passed/failed/blocked` 后，才能开启下一轮；第 1 轮未测完时不能开启第 2 轮。
- 复制时保留需求、标题、描述、类型、优先级、负责人、引用来源、AI 草稿标记、AI 预估和执行预估。
- 复制时清空执行状态、开始/完成时间、执行人、失败原因和阻塞原因，新轮次从 `pending` 开始。
- 测试进度、无 Bug 通过率和列表按当前轮次查看；顶部 AI 预估、执行预估、实际耗时、人力投入按版本全部轮次累计。
- 测试用例按所属需求分组时展示“已提测/待提测”标签；只有该需求下所有开发任务都 `submitted` 才展示“已提测”，否则展示“待提测”。

**理由**：第一轮承载完整测试范围，后续轮次应复跑同一范围而不是临时拼装；执行记录按轮次隔离，整体投入按版本累计，能同时回答“这一轮测得怎么样”和“这个版本测试总共花了多少”。
## 28. Daily report uses work activity log, not a generic rules engine

**Problem**: A daily report based only on manual `task-worklogs` misses important actions such as submitting a product plan, starting a development task, submitting code to test, fixing bugs, or marking blockers.

**Decision**: Add a lightweight `work-activities` document key and a typed `work-activity-factory.ts`. Business stores append activity records after successful operations. The workspace daily report derives a personal report from activities, legacy worklogs, and current work items.

**Why**:
- Automatic activity records provide evidence that work happened today.
- Manual progress notes explain multi-day work when no status changed today.
- A generic rules engine is too heavy for the current AppData stage and would hide business rules behind configuration.
- A pure aggregation function keeps report behavior testable and predictable.

**Rule**: Key status changes count as daily evidence. Multi-day in-progress work without today's activity or progress note is flagged as needing a progress update.

## 29. 计划日期使用中国节假日日历提示，不做硬阻断

**问题**：调研、产品方案、UI 设计、开发任务、测试用例和 Bug 修复都需要填写计划时间。原生日期控件样式不一致，也无法提示中国法定节假日和调休工作日。

**决策**：
- 新建统一的工作日日期时间选择组件，创建任务时复用同一套交互。
- 内置国务院办公厅发布的 2026 年中国法定节假日和调休工作日；未知年份按周末/工作日兜底。
- 选择节假日或周末时只提示，不阻止保存；选择调休工作日时按工作日提示。
- TestCase 增加 `plannedTestAt` / `plannedEndAt`，Bug 增加 `plannedFixAt`，保存为 ISO 时间戳。

**理由**：项目排期需要贴近中国工作日，但研发和线上 Bug 可能确实安排在非工作日处理，所以系统负责提醒，最终是否保存交给用户判断。

## 30. 正常工时走中国工作日历，加班单独计入

**问题**：接入中国节假日后，如果任务从节假日前开始、节假日后结束，直接按自然时间差会把休息日误算为实际耗时。

**决策**：
- `calcWorkHours` 和正常任务的 `calcActualElapsedHours` 都按中国工作日历和工作时段计算。
- 法定节假日、周末不计入正常工时；调休上班日计入正常工时。
- AI 预估工时是工作量建议，不按日期跳过；执行预估只有从计划起止时间自动推导时才走工作日历。
- 加班记录不受工作日历过滤，但不按自然小时连续打卡计算；使用项目管理口径：每个自然日默认 9:00-12:00、13:00-18:00 计 8h，中间完整日期按 8h，首尾日期按填写的开始/结束时间裁剪，结束晚于 18:00 的当天额外计入超出时长。

**理由**：正常任务耗时回答“工作时间里实际投入了多少”，加班记录回答“额外投入覆盖了多少项目工作量”。项目管理不做打卡，跨天记录不能把夜间空档算成工时；但节假日和周末仍允许记录，不受中国工作日历过滤。

## 31. AI 原型拆解优先按需求编号匹配，再按需求概述兜底

**问题**：原型文件里有些批注并不总是稳定写成 QY 编号，或者 QY 片段和需求之间没有显式绑定。仅按 QY 批注匹配会漏掉“原型里实际已有注释”的需求。

**决策**：
- 原型上下文提取不只围绕 QY 编号，也围绕当前版本关联需求的 `id`、`code`、`title`、`description` 命中片段。
- Agent 对账优先按需求编号匹配；原型文本出现 `requirement.id` 或 `requirement.code` 时必须优先归到该需求。
- 没有需求编号时，再按需求标题和需求概述做语义匹配。
- 没有 QY 编号但命中了需求编号或需求概述时，允许只引用 `requirement`，`matched.noteIds` 返回空数组。
- 编号和概述都匹配不到、但能形成明确功能名称和任务范围的原型批注，进入 `prototypeOnly` 无需求ID分组；只有无法稳定转化为任务/用例的批注才进入 `ambiguous`。

**理由**：需求编号是最稳定的对账锚点，需求概述是编号缺失时的业务语义兜底。把匹配证据先送进上下文，再用 prompt 明确优先级，比只依赖模型从截断文本里自由联想更稳定。

## 32. 测试用例类型细分，AI 拆解按测试点而不是大流程输出

**问题**：测试用例原先只有功能/API/异常/兼容性四类，AI 容易把多个交互、接口、数据状态和边界场景合并成一条“大用例”，导致测试范围不够细。

**决策**：
- 在测试分组增加细分类：UI 交互、表单校验、数据一致性、权限、边界值、状态流转、回归测试。
- 早期 AI tool schema、shared 类型和前端任务类型字典同步允许这些 `categoryCode`；后续这些类型只作为建议映射，AI 可通过 `taskTypeName` 输出字典外类型，见决策 #41。
- Prompt 明确要求 TestCase 按功能点、交互、接口、数据、权限、异常、边界、状态流转和回归点拆细。
- 一条 QY 如果同时涉及 UI、接口、数据、异常和状态变化，通常应拆出 3-8 条 TestCase，不允许用单条“完整流程验证”兜住。

**理由**：测试用例的分类粒度会反过来影响 AI 输出粒度。更细的稳定语义码能让模型把测试范围拆开，也让后续统计、筛选和负责人评估更准确。

## 33. AI 可推荐负责人，但必须由用户确认后才写入

**问题**：版本成员已经维护完成后，AI 拆解任务时完全不看成员会浪费上下文；但如果 AI 直接分配负责人，容易把模型建议误认为团队承诺，也可能编造成员姓名造成脏数据。

**决策**：
- AI 草案允许输出 `recommendedAssigneeName` 和 `recommendedAssigneeReason`，但姓名只能来自当前版本成员 `members[].name`。
- 前端用规则 helper 再校验推荐姓名是否属于当前版本成员，非成员姓名直接丢弃。
- 采纳弹窗默认不写入负责人；用户勾选“采纳推荐负责人”后，才把已校验推荐写入 DevTask/TestCase 的 `assigneeId`。
- 没有明确角色匹配或成员匹配时，AI 不输出推荐字段，任务保持未分配，供成员后续领取或手动分配。

**理由**：负责人推荐能减少项目经理初次分配成本，但分配本身是团队执行承诺，必须由人确认。把推荐和写入分开，可以复用版本成员上下文，又避免模型幻觉姓名或越权自动派单。

## 34. AI 草案领取和计划必须绑定

**问题**：AI 生成的 DevTask / TestCase 如果没有负责人，团队需要先领取；如果把“待领取”和“待排期”拆成两个可见状态，会让版本详情列表出现更多标签，且无法体现“领取时就应该承诺计划”的业务动作。

**决策**：
- 版本详情开发任务和测试用例不显示“待排期”标签。
- 无负责人时显示“待领取”，领取入口必须同时填写计划起止时间。
- 用户采纳 AI 推荐负责人后，草案已经有负责人，不再需要领取；但开始开发/测试前仍必须补齐计划起止时间。
- DevTask 进入 `in_progress` 前必须具备 `assigneeId`、`expectedStartAt`、`expectedEndAt`。
- TestCase 进入 `running` 前必须具备 `assigneeId`、`plannedTestAt`、`plannedEndAt`。

**理由**：领取代表成员承诺执行，计划时间代表承诺边界，二者应该在同一个动作里完成。列表层只表达“谁还没接手”，状态机层负责阻止未计划任务进入执行，页面不会被额外标签干扰。

## 35. 产品/UI 计划需求覆盖必须区分部分完成和完全完成

**问题**：产品方案和 UI 设计经常跨天推进，同一天可能只完成某条需求的一部分。旧的勾选式“引用需求已完成”只能表达完成/未完成，日报和后续分析无法知道本次完成了什么、还剩什么，也容易把部分完成误算成可提交成果。

**决策**：
- VersionPlan 增加 `requirementCoverage[]`，每条引用需求记录 `not_started / partial / completed`、已完成内容、剩余内容、更新人和更新时间。
- 旧的 `completedRequirementIds` 继续保留用于兼容历史数据，但当同一需求存在 `requirementCoverage` 时，以新覆盖状态为准。
- 产品/UI 计划的成果提交门禁只认 `completed`，`partial` 只记录进度，不满足“关联需求全部覆盖”。
- 计划增加 `logs[]`，记录需求进度更新和 AI 拆解触发/完成/失败，右侧日志时间线消费该数据。

**理由**：覆盖状态是产品/UI 计划自身的业务事实，不应该用简单 checkbox 表达。显式记录“已完成/剩余”能支撑日报、复盘和需求完成质量分析，同时保留旧字段可避免历史数据迁移成本。

## 36. 小宝预警规则优先，AI 只做解释

**问题**：如果直接让 AI 判断版本能否发版，模型可能忽略系统内的任务、Bug、测试、日报和权限事实，结论不可追溯；如果只按风险等级触发 AI，又会漏掉同等级内风险剧变，例如 P1 Bug 从 0 到 3、测试失败、发版日只剩 1 天。

**决策**：
- 小宝预警先由确定性规则计算 `riskScore`、`riskLevel`、`forecastReleaseDate`、`confidence`、趋势、静默风险和证据摘要。
- `on_track` 不触发 AI；`at_risk`、`likely_delayed`、`blocked` 自动触发 AI；`attention` 只有在风险分、趋势、关键 Bug、失败用例、阻塞、静默风险、置信度或预测日期出现明显恶化时触发。
- AI 解读自动触发，不提供人工“AI 解读”按钮。缓存签名必须覆盖趋势、原因、静默风险、日报/活动证据、风险信号和置信度，避免复用过期解读。
- 快照保存需要节流：同版本同日普通变化 10 分钟内不重复保存；风险等级变化、风险分变化达到阈值、关键 Bug/失败用例/阻塞/静默风险变化或预测日期明显变化时立即保存。
- AI 解读需要 cooldown：同版本最近 6 小时内已有解读时不重复请求；如果风险等级升级，则允许绕过 cooldown。
- AI 只写入 `xiaobao-risk-insights` 缓存，不修改 Version、DevTask、TestCase、Bug、Requirement 或 Member。

**理由**：规则结果可测试、可追溯、可复盘；AI 文案提升可读性，但不能替代系统事实判断。趋势、静默风险和置信度能弥补“当前风险等级”过于静态的问题。

## 37. AI 拆解支持无需求ID分组，不自动补需求池

**问题**：0 到 1 项目中，需求池可能只录入一条宽泛需求，例如“做一个充值功能”，而原型批注里已经包含更多细粒度功能。旧规则把“原型里有、关联需求里没有”的批注只放进 `noteOnly` 报告，不拆任务，会漏掉真实开发和测试范围。若让 AI 自动创建 Requirement 又会污染需求池和版本关联需求列表，让“正式需求范围”和“原型推断内容”混在一起。

**决策**：
- AI 拆解继续优先按关联需求匹配；匹配成功的草案带正式 `requirementId`。
- 原型中明确可拆、但没有匹配到关联需求的批注，进入 `prototypeOnly`，也可以生成 DevTask / TestCase 草案。
- `prototypeOnly` 草案不创建 Requirement，不加入需求池，也不加入版本关联需求列表。
- 这类草案写入开发任务/测试用例时，`requirementId` 为空，使用 `requirementName` 作为展示分组名，列表标题为 `无需求ID · {requirementName}`。
- 不额外展示“原型发现”标记；只有既有 `aiDraft` 草案视觉状态。
- DevTask 需要直接归属版本（`versionId`），`requirementId` 改为可选；旧数据可继续通过 `requirementId -> Requirement.versionId` 兼容推导。
- 版本级统计、工作台和风险预警统计包含无需求ID分组任务/用例；需求级进度和需求关闭条件只统计带正式 `requirementId` 的 DevTask。

**理由**：关联需求代表人为确认的正式范围，不能被 AI 静默扩写；但原型批注也是当前版本真实交付范围的重要证据，不应该因为没有 REQID 被丢掉。无需求ID分组让任务和用例完整进入执行视图，同时保持需求池干净、关联需求列表可信。

## 38. 生产部署采用 Docker Compose + Nginx 同域反代

**问题**：系统已经从浏览器 localStorage 迁到 NestJS + PostgreSQL `app_data`，可以部署到云服务器，但如果只依赖开发命令，生产环境会缺少统一编排、健康检查、持久卷、反向代理和数据库初始化流程。

**决策**：
- 生产部署使用 `docker-compose.prod.yml` 编排 `web`、`server`、`postgres`、`redis`、`nginx`。
- 本地服务器/局域网部署使用 `docker-compose.local.yml`，同样跑完整五服务栈，但默认绑定宿主机 `8080`，并使用独立 `local_*` volumes。
- `web` 和 `server` 分别用 `Dockerfile.web`、`Dockerfile.server` 从 monorepo 根目录构建，先构建 `@ftb/shared`，再构建各自应用。
- 对外只暴露 Nginx 80 端口；`/api/` 转发到 NestJS，其他路径转发到 Next.js。
- 前端生产默认使用同域 API：`NEXT_PUBLIC_API_URL=/api/v1`，避免浏览器跨域配置。
- PostgreSQL、Redis 和 server 文件数据分别使用 `postgres_data`、`redis_data`、`server_data` 命名卷持久化。
- 数据库生产初始化走 `prisma migrate deploy`，仓库保留初始 migration；本地开发仍使用 `prisma migrate dev`。
- HTTPS 先交给云负载均衡、CDN、宿主机证书工具或外层 Nginx 终止；Compose 内置 Nginx 保持 HTTP 反代基线。

**理由**：Docker Compose 足够覆盖当前单机云服务器和本地服务器形态，部署成本低、可读性强，也符合现阶段 2 核 4G 云主机目标。同域反代能减少 CORS 和公网端口暴露面。本地服务器默认 8080，避免占用 80 端口或要求管理员权限；云服务器继续使用 80 作为外层入口。HTTPS 证书自动续期和域名接入在不同云环境差异较大，先作为外层能力处理，避免把生产部署模板绑死在某一种证书方案上。

## 39. AppData 文档写入采用乐观锁，先阻止静默覆盖

**问题**：V2.1 阶段业务数据仍按模块存成 `app_data.value` 整份 JSON 文档。多人同时打开同一模块后，如果 A 和 B 都基于旧副本编辑，原来的无条件 `upsert` 会让后保存的人覆盖先保存的人，尤其是任务、Bug、成员等高频写入数据。

**决策**：
- `GET /api/v1/data/:key` 返回 `version`，由 `AppData.updatedAt.toISOString()` 派生；空文档返回 `version: null`。
- 前端 `server-data.ts` 在读取和保存成功后缓存每个 key 的最新 `version`。
- 前端保存已读取过的 key 时提交 `{ value, version }`；尚未读取过的兼容路径仍可提交 `{ value }` 走旧式 upsert。
- 后端收到字符串 `version` 时使用 `updateMany({ where: { key, updatedAt }, data: { value } })` 原子比较并更新；`count !== 1` 返回 `409 APP_DATA_CONFLICT`。
- 后端收到 `version: null` 时只允许创建不存在的行；如果其他客户端已经创建，返回同样的 `409 APP_DATA_CONFLICT`。
- 冲突响应返回 `currentVersion` 和 `currentValue`，但当前前端不自动合并、不自动重试，避免把旧本地副本用新版本号再次覆盖服务端数据。

**理由**：这是 AppData 阶段成本最低、收益最高的一致性补强。它不能提供字段级协同编辑，但能阻止最危险的“静默最后写入覆盖”。后续拆成关系表和领域 API 后，再在具体实体上做更细粒度的事务、唯一约束、审计日志和冲突合并 UI。

## 40. AI 拆解借鉴 Superpowers 的交付切片和测试倒逼粒度

**问题**：AI 原型拆解虽然已经要求细颗粒任务和测试用例，但模型仍可能把清晰 QY 批注压成一条大任务、一条“完整流程验证”，或者把带详细说明、字段和验收标准的批注误判为含糊。

**决策**：
- Prototype Decompose Agent 在生成 DevTask/TestCase 前，必须先按 UI、交互、接口、数据、异常、边界、状态流转、兼容和回归做内部交付切片。
- 每条业务规则、字段规则、交互规则、异常规则和验收标准，必须至少映射到 1 条开发任务或 1 条测试用例；本次 target 不包含的一侧可以不输出，但另一侧必须覆盖。
- DevTask `description` 从可选变为必填，至少包含实现范围和验收点。
- TestCase `description` 必须包含前置条件、操作步骤和预期结果；涉及边界、异常或数据一致性时写明测试数据或状态。
- 原型批注只要包含标题、详细说明、字段、异常或验收标准之一，且能判断用户动作或系统行为，就不得进入 `ambiguous`；必须进入 `matched` 或 `prototypeOnly` 继续拆解。

**理由**：Superpowers 的任务计划和 TDD 流程本质上是“先把工作拆成可验证切片，再用测试约束倒逼粒度”。把这套方法沉淀到 Agent 契约里，能减少粗拆、漏拆和误判含糊，同时不改变现有数据模型，只提高 AI 草案的结构质量。

## 41. AI 拆解任务类型不受人工字典限制，采纳时自动入库

**问题**：任务类型字典最初服务于人工新建任务表单，覆盖的是常见开发和测试类型。AI 从原型里识别出的真实交付切片可能更细，例如“人员名片交互”“预入职数据源”“姓名展示兼容测试”。如果继续要求 AI 只能输出既有 `categoryCode` 枚举，就会把拆解能力绑死在人工表单选项上，字典没覆盖时容易漏拆或被迫归到错误类型。

**决策**：
- AI DevTask / TestCase 草案必须输出 `taskTypeName`，这是展示给用户的可复用任务类型名称，不是任务标题或当前文档的场景概述。
- `categoryCode` 改为可选兼容映射；只有能明确对应已有稳定语义码时才输出，不再作为 schema 必填项，也不再限制为固定枚举。
- 前端采纳 AI 结果时，按 `taskTypeName` 在当前 `TaskCategory` 同分组内查找；可复用开发类型不存在时自动追加一条 `isSystem=false` 的任务类型，并用该类型的 `id` 写入 DevTask。测试用例未知类型不自动入库，优先按 `categoryCode` 或默认测试类型回退。
- 去重逻辑按任务类型名称、标题和引用来源判断；已采纳过的自定义 AI 类型再次生成时也能过滤重复草案。
- 人工新建任务表单继续使用任务类型字典作为可选项，但这个字典会随着 AI 草案采纳自动扩展。

**理由**：人工表单的任务类型是录入辅助，不应该成为 AI 拆解边界。但任务类型字典也不能被单个原型的业务对象污染。`taskTypeName` 只承载可复用分类，具体功能点放在 `title` 和 `description`；采纳时有门槛地补字典，让后续筛选、统计和人工创建复用真正稳定的类型，同时避免 AI 直接写数据库 `categoryId`。
## 42. V2.2 高增长业务表从一开始采用分区表

**问题**：需求池、开发任务、测试用例、BUG、工作活动和小宝快照会按年持续增长。若先用普通表，后续再切分区表，不只是搬旧数据，还会牵动主键、唯一约束、外键、迁移窗口和回滚方案。

**决策**：
- `requirements` 按 `product_id` 做固定 HASH 分区，主键为 `(id, product_id)`。
- `dev_tasks` / `test_cases` / `bugs` 按 `version_id` 做固定 HASH 分区，主键为 `(id, version_id)`。
- `work_activities` / `task_worklogs` / `overtime_records` / `xiaobao_risk_snapshots` / `xiaobao_risk_insights` / `ai_logs` 按 `created_at` 做 RANGE 分区并保留 default partition。
- 所有分区表主键和业务唯一约束必须包含分区键，例如 `(version_id, code)`。
- 引用分区表时优先使用复合外键，例如 `(requirement_id, requirement_product_id)`、`(test_case_id, test_case_version_id)`；多态活动记录使用逻辑外键。
- `xiaobao_risk_summaries` 不分区，保持 `version_id` 单行汇总；历史趋势进入分区快照表。

**理由**：把分区键纳入主键和唯一约束是 PostgreSQL 分区表的结构性要求。提前做这件事，可以避免客户数据变大后再重塑主键和外键。固定 HASH 分区避免每个项目/版本单独建分区的维护负担；RANGE 分区只用于天然追加、按时间维护的历史数据。

## 43. V2.3 AppData 写入后非阻塞同步关系表

**问题**：V2.2 已经让版本详情、需求池、与我相关和小宝预警优先读取关系表，但前端保存仍然写 AppData。如果关系表不跟随 AppData 更新，快读路径会逐渐变旧，最后又回落到加载大 JSON 文档，无法解决几十万条需求/任务/用例后的卡顿风险。

**决策**：
- AppData 继续作为兼容窗口内的写入事实源，`DataService.put()` 先完成乐观锁写入，再触发关系表同步。
- 同步服务独立为 `AppDataV23SyncService`，复用 V2.2 mapper，不把一次性迁移服务改造成在线写入服务。
- 当前态表按分区键作用域替换：需求按 `product_id`，版本计划/开发任务/测试用例/BUG 按 `version_id`。
- 追加型证据表继续 `createMany(skipDuplicates)`，不因当前 AppData 文档缺失而删除历史。
- 小宝摘要由快照刷新；计划/任务/用例/BUG/活动/工时/加班变更只标记摘要 `dirty=true`，等待下一次规则计算刷新完整内容。
- 同步失败只记录日志，不阻塞 AppData 保存。慢 API 和慢 Prisma 查询先通过日志监控，后续再接 Prometheus/Grafana。

**理由**：这是从 AppData 兼容写入平滑过渡到领域 CRUD 的中间层。用户保存不能因为派生关系表暂时失败而丢失业务数据；同时，关系表保持跟随更新后，V2.2 快读路径才能真正承受大数据量。把同步服务独立出来，也能让后续领域 CRUD 逐步替换 AppData 时复用同一套映射和小宝 dirty 策略。
