# 关键设计决策记录 每条决策都包含**为什么这么做**,避免后续模型重新讨论或推翻。 ## 1. 进度计算 — 由状态推导,不要手填百分比 **问题**:Jira/TAPD 等系统让开发手填进度(20%/35%/50%),数据完全主观,不可靠。 **决策**:DevTask 进度由 `status` 推导: - `todo` → 0% - `in_progress` → 50% - `testing` → 80% - `submitted` → 100% **理由**:客观、不允许造假。需求级/版本级进度 = `Σ(estimateHours × 状态推导%) / Σ(estimateHours)`。 ## 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` **理由**:阻塞和状态正交。工作台筛选 `isBlocked=true` 一键拉出所有阻塞项,跨状态。 ## 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 输出**对账报告**,三段: - ✅ 完美对应(需求 ↔ 原型注释 ↔ 任务) - ⚠️ 仅需求未见原型 / 仅原型未见需求 - ❓ 注释含糊无法转化 **理由**:让历史版本入库的成本远高于让用户对账的成本。对账报告本来就是 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`。 **理由**: - 完成条件属于业务规则,不属于按钮组件;集中到工作流引擎后,列表和抽屉能保持一致。 - 需求候选来源属于领域边界;统一 selector 能避免从需求池、项目需求、版本需求之间误取数据。 - AI 不应该猜数据库 id;稳定语义码能兼容管理员调整展示名称,也方便后续扩展更多模型。 - 测试用例带任务类型后,才能知道测试覆盖范围,AI 拆解和人工创建的数据形状也一致。