Files
ftb-project-management/docs/decisions.md
2026-07-08 16:07:39 +08:00

611 lines
48 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.

# 关键设计决策记录
每条决策都包含**为什么这么做**,避免后续模型重新讨论或推翻。
## 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 字典表 + groupdevelopment/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 证书自动续期和域名接入在不同云环境差异较大,先作为外层能力处理,避免把生产部署模板绑死在某一种证书方案上。
## 38.1. 生产发布改为 CI 镜像制品 + 运行版本校验
**问题**:仅让部署侧 `git pull` 最新代码并不能保证线上用户看到新前端。Next.js 前端需要重新构建Docker 容器也需要重新创建;如果对方只拉代码、不 `build/up`,就会出现仓库是新的、运行容器仍是旧的情况,需求池搜索、成员用户名、性能改动等都无法靠肉眼判断是否已经上线。
**决策**
- GitHub Actions 在 `master` 更新时构建 `web` / `server` Docker 镜像,并推送到 GHCR。
- 镜像以 commit SHA 作为不可变 tag同时更新 `master` tag。
- `docker-compose.prod.yml` 使用 `WEB_IMAGE` / `SERVER_IMAGE` 拉取镜像,保留 `build` 仅作为本地兜底。
- `APP_VERSION` 使用 commit SHA构建时写入前端 `NEXT_PUBLIC_APP_VERSION` 和后端运行环境。
- 后端提供 `GET /api/v1/health/version`Actions 发布结束后必须校验返回版本等于本次 commit SHA。
- 前端定时比较自身构建版本和服务端运行版本,不一致时提示刷新页面。
**理由**:发布物必须是 CI 产出的镜像,而不是服务器上的源码目录。版本号打进镜像后,部署问题可以被机器判断:如果 Actions 校验通过,说明线上容器已经运行本次提交;如果校验失败,问题就在部署链路而不是业务代码。前端刷新提示解决的是浏览器仍持有旧 bundle 的尾部问题,不能替代容器更新,但能让用户明确知道需要刷新。
## 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 策略。
## 44. V2.4 领域 CRUD 成为主写入路径AppData 退为兼容兜底
**问题**V2.2/V2.3 让高增长页面优先读关系表,但前端主写仍长期停留在 AppData 时会形成“AppData 写入 + 关系表同步”的双层事实链。数据量继续增长后,需求池分页搜索、版本详情、与我相关和小宝预警仍会受 AppData 同步时效、整文档写入和双源理解成本影响。
**决策**
- V2.4.0 先统一共享状态契约,避免领域 API 切换时把旧状态机重新带回系统。
- V2.4.1-V2.4.4 将 Product、Project、Version、Requirement、VersionPlan、DevTask、TestCase、Bug 切为领域 API 主写。
- V2.4.5 将 Member、TaskCategory、TaskWorklog、OvertimeRecord、WorkActivity 切为领域 API 主写。
- 需求池列表/search/filter/sort 走服务端分页,避免加载全量 AppData 文档。
- 版本详情实体按 `versionId` 分区键写入Requirement 按 `productId` 分区键写入;追加型证据表保留追加语义,不从 AppData 快照反向删除历史。
- `/workspace``/xiaobao-warning` 继续走关系表聚合/快读。领域写成功后通过工作活动和小宝 dirty 标记维持证据链。
- AppData 保留为兼容读取、失败回退、历史迁移和少量配置承载,不再作为已迁移领域的事实源。
- 成员迁移采用保守边界:`users` 存成员身份字段;部门、角色、密码规则暂不在本阶段发明完整 RBAC 表,仍作为 AppData 兼容配置。
- 加班原因同样暂留 AppData 配置;加班记录本身写 `overtime_records`
**理由**
- 领域 CRUD 直接写关系表后,读写路径对齐,分页、筛选、聚合和风险预警不再依赖 AppData 同步是否及时。
- 分区键进入每次领域写入,能维持 V2.2 分区表设计的查询边界。
- AppData fallback 让迁移可回滚、可兼容旧数据,但不再制造长期双事实源。
- RBAC/配置表会影响权限模型和管理流程单独成阶段更安全V2.4.5 只收口当前高频业务写入,避免为了“全收口”临时设计不稳的权限 schema。
## 45. V2 后端关系化采用八阶段交付链路
**问题**V2.1/V2.2/V2.3 已经分别解决了服务端持久化、关系表快读和 AppData 写入后同步关系表。后续如果只写“AppData 退场、权限审计、性能增强、企业能力、生产稳定”这些大方向容易出现三个偏差V2.4 写领域 CRUD 时没有提前埋权限和审计V2.5 把 AppData 当成可直接删除的旧表V2.6 才发现分页、索引和查询边界没有在 API 设计期处理。
**决策**
- V2.1AppData 服务端持久化,先把业务数据从浏览器 localStorage 迁到服务端。
- V2.2:关系表 + 分区 + 快读 API优先支撑版本详情、需求池、工作台和小宝预警等读热点。
- V2.3AppData 写入后同步关系表,兼容期内保证快读数据跟随更新。
- V2.4:领域 CRUD 主写迁移。每个领域 API 从第一版就必须带资源作用域、当前用户、`actorId`、基础审计事件入口、分页、索引和分区键查询边界。
- V2.5AppData 分阶段退场 + RBAC/审计/一致性收口。退场顺序固定为“禁写 → 双读核对 → 移除 fallback → 只读归档/导出 → 后续删表”。
- V2.6:大数据性能增强 + 小宝预警后台化。在关系表主源稳定后做压测、慢查询治理、缓存/摘要、后台任务、幂等、锁和失败重试。
- V2.7:企业级协作能力 + 管理治理,补通知、协同、组织治理、管理视图和企业级配置。
- V2.8:生产硬化稳定版 + 运维闭环。生产部署基线已存在V2.8 聚焦备份恢复演练、发布 smoke test、监控告警、日志检索、迁移回滚和运维手册。
**理由**:这条链路保持了从低风险兼容到强一致主源的顺序。权限/审计必须随领域 CRUD 进入代码路径否则后补会重写接口边界AppData 退场必须有闸门和回滚价值,不能直接删除;性能基础要从 V2.4 的 API 设计开始V2.6 只做规模化增强和后台化能力。这样每个阶段都有清晰验收物,也能避免长期双主源、无审计写入和大数据查询返工。
## 46. 业务主数据源切换到 PostgreSQL 领域关系表
**问题**AppData JSONB 文档表解决了浏览器 localStorage 丢数据问题,但如果继续把 `app_data.value` 当长期主数据源,会带来三个风险:整份 JSON 写入难以做字段级事务和权限校验,大数据量下筛选/分页/统计仍要依赖派生同步,线上排查时容易出现 AppData 与关系表不一致。
**决策**
- PostgreSQL 领域关系表是后续业务主数据源AppData 只保留为迁移、回填、兼容读取和审计排查入口。
- 新增业务模块不得新增 AppData key 作为主存储必须先设计关系表、Prisma model、领域 CRUD API 和必要的索引/分区键。
- 现有 AppData key 按领域逐步迁移:先补关系表写 API再让前端 store 写领域 API最后移除对应 `saveServerData/loadServerData` 主路径。
- V2.2 快读失败时的 AppData fallback 只能作为迁移期兜底,不能通过硬编码 `usingV22=false` 长期绕开关系表。
- 删除或停用 JSON 文档前必须完成数据备份、迁移计数核对、抽样校验和回滚预案。
- 一次性同步脚本可以存在,但必须作为运维迁移工具管理,不能依赖提交 `.env` 或手工修改源码开关。
**理由**系统未来要承载大量需求、任务、测试用例、Bug、活动和风险数据。关系表才能提供可验证的约束、事务、索引、分页、权限和审计能力。AppData 是低风险迁移桥,不是最终架构;继续扩大 JSON 主存储会把数据一致性和性能问题推迟到更难修的阶段。