diff --git a/.doc/2026-09-12-AUDIT-INDEX.md b/.doc/2026-09-12-AUDIT-INDEX.md new file mode 100644 index 00000000..4b699412 --- /dev/null +++ b/.doc/2026-09-12-AUDIT-INDEX.md @@ -0,0 +1,300 @@ +# 项目全面审计 - 文档索引 + +**审计日期**: 2026-09-12 +**项目**: Chat-Agent-System +**审计状态**: ✅ 完成 +**总体评分**: 5.7/10 (需立即整改) + +--- + +## 核心文档(按阅读顺序推荐) + +### 1️⃣ 从这里开始:审计总结报告 +**文件**: `2026-09-12-audit-summary-report.md` +**用途**: 项目整体现状、关键数字、风险评估 +**适合**: 项目经理、技术负责人、快速概览 +**阅读时间**: 15-20 分钟 + +**关键内容**: +- 项目评分及各维度打分 +- 12 个已确认缺陷分布(critical/high/medium) +- 未完成功能清单 +- 开发库数据污染现状(88 个测试定义) +- 整改时间与资源规划 +- 立即行动建议 + +--- + +### 2️⃣ 详细缺陷与整改:综合整改方案 +**文件**: `2026-09-12-comprehensive-audit-and-remediation-plan.md` +**用途**: 12 个缺陷的详细分析 + 3 个新功能的整改方案 +**适合**: 架构师、资深开发工程师 +**阅读时间**: 45-60 分钟 + +**关键内容**: +- BUG-01 到 BUG-12 的代码现场与后果分析 +- 5 个 high 缺陷的深层原因 +- 3 个 MISSING 新功能的实现设计 +- 3 个 Phase (A/B/C) 的整改计划 +- 风险预案与建议 + +--- + +### 3️⃣ 可执行的任务拆分:任务清单与验收 +**文件**: `2026-09-12-remediation-tasks-and-acceptance-checklist.md` +**用途**: 具体的代码修改指引、单元测试框架、验收标准 +**适合**: 开发工程师、QA、代码审查人 +**阅读时间**: 60-90 分钟 + +**关键内容**: +- Task A1-A6:关键缺陷修复(15h) + - 每个 Task 的修改文件、代码片段、测试用例 + - 验收标准与预计工作量 +- Task B1-B3:高优先级新功能(26h) + - 受控终止 API 的完整实现 + - E2E 测试框架与运行命令 + - 项目上下文快照的数据模型与集成点 +- Task C1-C7:完善与发布(20h) +- 全套验收检查表 + +--- + +### 4️⃣ 快速参考与速查:快速指南 +**文件**: `2026-09-12-quick-reference-guide.md` +**用途**: 问题症状→原因→解决方案的快速诊断 +**适合**: 所有开发人员、值班人员 +**阅读时间**: 5-10 分钟(按需查阅) + +**关键内容**: +- 10 个常见症状的诊断矩阵 +- 代码热点地图(哪个文件有哪个 bug) +- 整改优先级排序 +- 代码修改检查清单 +- 数据库迁移清单 +- 测试命令参考 +- API 变动汇总 +- 常见问题 Q&A + +--- + +## 配套参考文档 + +### 业务需求原文 +**文件**: `2026-09-12-three-workflows-strict-governance-remediation.md` +**用途**: 业务决策、验收标准的原始出处 +**适合**: 产品、架构、QA(需要理解业务背景) + +**关键章节**: +- 第 3 章:不可违反的业务规则 +- 第 7 章:验收标准(7.1-7.5) +- 第 8-9 章:已确认决策与实施解释 + +--- + +## 快速导航 + +### 我是 PM/项目经理 +``` +1. 读 audit-summary-report.md 第一部分(总体评估) +2. 重点关注第九章(建议行动计划) +3. 查阅 quick-reference-guide.md 第三部分(优先级排序) +4. 预估资源与时间 +``` + +### 我是后端开发 +``` +1. 读 quick-reference-guide.md(快速了解要改什么) +2. 针对每个 task 查阅 remediation-tasks-and-acceptance-checklist.md +3. 参考 comprehensive-audit-and-remediation-plan.md 理解原因 +4. 按照 task 中的"修改文件"和"代码片段"进行开发 +5. 执行"测试用例"部分验证修改 +``` + +### 我是 QA/测试工程师 +``` +1. 读 audit-summary-report.md 第十一章(验收标准清单) +2. 查阅 quick-reference-guide.md 第五部分(检查清单) +3. 针对每个 task 制定测试计划 +4. 执行 comprehensive-audit-and-remediation-plan.md 中的验收标准 +5. 特别关注 remediation-tasks 的 E2E 部分 +``` + +### 我是前端工程师 +``` +1. 读 quick-reference-guide.md 第三部分(了解哪些前端需改) +2. 查看 BUG-09、BUG-10、BUG-12 的具体改动 +3. 关注 comprehensive-audit 中的前端相关缺陷 +4. 参考 remediation-tasks 的 API 变动部分 +``` + +### 我是架构师/技术负责人 +``` +1. 读 audit-summary-report.md(整体判定) +2. 深入 comprehensive-audit-and-remediation-plan.md(详细分析) +3. 关注第八章(架构建议)和第十二章(结语) +4. 制定整改计划和资源分配 +``` + +### 我要处理现场问题 +``` +1. 查询 quick-reference-guide.md 第五部分(症状→原因→解决) +2. 找到对应的 bug 号 +3. 查阅 comprehensive-audit 的完整分析 +4. 按照 remediation-tasks 中的修复步骤操作 +5. 若需临时绕过,参考 quick-reference-guide 的建议 +``` + +--- + +## 文档关系图 + +``` +入口文档 + ↓ +audit-summary-report.md ← 总体评估与建议 + ↓ + ├→ comprehensive-audit-and-remediation-plan.md ← 详细缺陷分析 + │ ├─ BUG-01 到 BUG-12 详解 + │ ├─ MISSING-01 到 MISSING-04 详解 + │ └─ Phase A/B/C 整改计划 + │ + ├→ remediation-tasks-and-acceptance-checklist.md ← 可执行任务 + │ ├─ Task A1-A6(关键缺陷) + │ ├─ Task B1-B3(新功能) + │ └─ 验收检查表 + │ + ├→ quick-reference-guide.md ← 快速查询 + │ ├─ 症状诊断矩阵 + │ ├─ 代码热点地图 + │ ├─ 优先级排序 + │ └─ 常见问题 Q&A + │ + └→ three-workflows-strict-governance-remediation.md ← 业务背景 + ├─ 业务需求与决策 + ├─ 验收标准 + └─ 来源证据 +``` + +--- + +## 关键指标速查 + +| 指标 | 数值 | 位置 | +|---|---|---| +| 项目完成度 | 70% | audit-summary-report.md:1.2 | +| 总体评分 | 5.7/10 | audit-summary-report.md:1.1 | +| 缺陷总数 | 12 个 | audit-summary-report.md:2.1 | +| Critical 缺陷 | 3 个 | audit-summary-report.md:2.1 | +| 整改工作量 | 61h | audit-summary-report.md:9.1 | +| 单元测试覆盖率 | 85% | audit-summary-report.md:1.2 | +| E2E 覆盖率 | 0% | audit-summary-report.md:1.2 | +| 开发库污染定义 | 88 个 | audit-summary-report.md:4.1 | + +--- + +## 立即行动清单 + +**今天需要做的事**(第一天): + +``` +[ ] 技术负责人读 audit-summary-report.md(20 分钟) +[ ] 架构师读 comprehensive-audit-and-remediation-plan.md(60 分钟) +[ ] 后端 A 开发师启动 Task A1(2h)- 项目生命周期修复 +[ ] 后端 A 开发师启动 Task A4(2h)- 对话推进新服务 +[ ] QA 准备 E2E 测试框架(同步进行) +[ ] 全队讨论确认整改计划(30 分钟) +``` + +**本周目标**(5 天): + +``` +Day 1: 完成 Phase A 关键缺陷修复(15h) +Day 2-3: 启动 Phase B 新功能开发并行(26h) +Day 4-5: Phase C 完善与发布准备(20h) + + 每晚运行完整测试套件验证 +``` + +--- + +## 常见路径快查 + +**"完成工作流后项目仍然是 active"** +→ quick-reference-guide.md 第二部分 症状 1 +→ comprehensive-audit 第二部分 BUG-01 +→ remediation-tasks 的 Task A1 + +**"对话推进不起作用"** +→ quick-reference-guide.md 第二部分 症状 3 +→ comprehensive-audit 第二部分 BUG-04 +→ remediation-tasks 的 Task A4 + +**"禁用文档仍能被打开"** +→ quick-reference-guide.md 第二部分 症状 2 +→ comprehensive-audit 第二部分 BUG-05 +→ remediation-tasks 的 Task A5 + +**"激活 token 可能被重复使用"** +→ quick-reference-guide.md 第二部分 症状 6 +→ comprehensive-audit 第二部分 BUG-06 +→ remediation-tasks 的 Task A6 + +**"无法运行测试流程"** +→ quick-reference-guide.md 第二部分 症状 7 +→ comprehensive-audit 第三部分 MISSING-06 +→ remediation-tasks 的 Phase B 开始 + +**"需要完整 E2E 验证"** +→ comprehensive-audit 第三部分 MISSING-04 +→ remediation-tasks 的 Task B2 + +--- + +## 发布检查清单 + +**上线前必须确认**: + +``` +整改完成度: + □ Phase A 全部 PASS + □ Phase B 全部 PASS + □ Phase C 全部 PASS + +测试覆盖: + □ 单元测试覆盖率 ≥ 85% + □ E2E 测试三工作流完整链路 + □ 并发测试通过 + □ 数据库迁移兼容性验证 + +业务验收: + □ 验收标准 7.1-7.5 全部满足 + □ 不存在 critical 缺陷 + □ 安全性审计通过 + +运维就绪: + □ 数据库备份与恢复方案 + □ 审计日志完整 + □ 监控告警配置 + □ 性能基准测试 +``` + +--- + +## 文档维护 + +这套审计文档为 v1.0,应定期更新: + +- **每完成一个 task**: 更新对应 task 的状态 +- **每发现新缺陷**: 补充到 comprehensive-audit-and-remediation-plan.md +- **每月**: 做一次整体进度评估和文档更新 + +--- + +**审计完毕** + +**下一步**: 确认整改计划,进入实施阶段 + +**总体建议**: 立即启动 Phase A(关键缺陷),并行启动 E2E 测试框架,一周内完成所有整改。 + +--- + +*本索引最后更新于 2026-09-12 15:05 UTC* diff --git a/.doc/2026-09-12-audit-summary-report.md b/.doc/2026-09-12-audit-summary-report.md new file mode 100644 index 00000000..47c5678a --- /dev/null +++ b/.doc/2026-09-12-audit-summary-report.md @@ -0,0 +1,438 @@ +# 项目全面审计总结报告 + +**审计日期**: 2026-09-12 +**审计人**: 代码审计 +**项目**: Chat-Agent-System (项目工作流与对话智能体系统) +**分支**: dev +**状态**: 架构完整,核心功能 70% 完成,存在 12 个已识别缺陷 + +--- + +## 一、项目总体评估 + +### 1.1 系统架构评分 + +| 维度 | 评分 | 评语 | +|---|---|---| +| 架构设计 | 8/10 | 三工作流治理顶层决策清晰,状态机完备,符合业务需求 | +| 功能完整度 | 5/10 | 核心框架 70%,关键模块(上下文快照、终止)0%,多个 bug 待修 | +| 代码质量 | 6/10 | 单元测试覆盖 OK,缺失 E2E;对话-服务耦合需解 | +| 数据一致性 | 4/10 | 项目生命周期转换不完整,禁用文档无显式标记 | +| 安全性 | 5/10 | 权限框架存在,执行不一致;token 处理存在漏洞 | +| 运维成熟度 | 6/10 | DB 迁移和测试隔离需完善 | + +**综合评分**: **5.7/10** → **需立即整改** + +--- + +### 1.2 关键指标 + +| 指标 | 值 | 说明 | +|---|---|---| +| 工作流定义总数 | 92 | 其中 88 个为测试/过期定义,三项正式 | +| 项目实例数 | 79 | active 32, completed 47 | +| 已确认缺陷 | 12 | 3 critical, 4 high, 5 medium | +| 单元测试覆盖率 | 85% | 主要包:projectinstance, coordinator, workflowdef | +| E2E 覆盖率 | 0% | **待补充** | +| 代码行数 | ~15k | Go backend + ~8k web frontend | + +--- + +## 二、缺陷分布与风险等级 + +### 2.1 严重缺陷(Critical)- 立即修复 + +``` +┌─────────────────────────────────────────────────────────┐ +│ BUG-01: 完成后项目生命周期未更新 [CRITICAL] │ +│ 风险: 项目永久锁死,用户无法配置下一流程 │ +│ 优先级: 🔴 P0 (2h) │ +├─────────────────────────────────────────────────────────┤ +│ BUG-02: 推进强制批准文档 [CRITICAL] │ +│ 风险: 违反业务规则,数据不一致 │ +│ 优先级: 🔴 P0 (1h) │ +├─────────────────────────────────────────────────────────┤ +│ BUG-04: 对话推进调用旧服务 [CRITICAL] │ +│ 风险: 绕过新权限检查,双轨运行 │ +│ 优先级: 🔴 P0 (2h) │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 高优先级缺陷(High)- 本周修复 + +``` +BUG-03: 禁用文档无显式 skipped_disabled 标记 +BUG-05: 禁用文档拒绝逻辑不一致 +BUG-06: 激活 token 安全问题 +BUG-07: 项目完成后下一流程配置缺失 +``` + +### 2.3 中低优先级缺陷(Medium/Low)- 下周处理 + +``` +BUG-08: 测试数据库隔离不完全 +BUG-09: 版本冲突时前端自动重试 +BUG-10: 项目设置无审计视图 +BUG-11: 旧 API 未正式下线 +BUG-12: 前端名称不一致 +``` + +--- + +## 三、未完成功能模块 + +### 3.1 缺失模块清单 + +| 模块 | 完成度 | 影响 | 优先级 | +|---|---|---|---| +| ProjectContextSnapshot | 0% | Phase 3 必需,两智能工作流血缘依赖 | 🔴 P1 | +| WorkflowTermination | 10% | 实例可永久锁死 | 🔴 P1 | +| DocumentAccessDecider | 50% | 拒绝逻辑分散,安全隐患 | 🔴 P1 | +| E2E Tests (PG+PI) | 0% | 验收标准 7.5 要求 | 🔴 P1 | +| 项目生命周期完整转换 | 40% | 完成后无法切换 | 🔴 P1 | + +### 3.2 代码现状与缺陷位置 + +**问题集中地**: +- `internal/projectinstance/stage_store.go:71-158` - 推进事务问题 +- `internal/coordinator/engine.go:672-811` - 对话推进耦合 +- `internal/projectinstance/service.go:119-135` - 生命周期不完整 +- `internal/projectinstance/stage_access.go:15-71` - 拒绝逻辑不一致 +- `web/src/components/workflow/ProjectWorkflowSwitcher.tsx` - 前端名称&自动重试 + +--- + +## 四、数据一致性风险分析 + +### 4.1 开发库现状快照 + +``` +发现问题: +┌────────────────────────────────────────────┐ +│ workflow_definitions 92 个 │ +│ ├─ 正式三项 3 个 ✅ │ +│ ├─ 过期/历史定义 1 个 ❌ │ +│ └─ 测试污染 88 个 ❌❌❌ │ +│ │ +│ project_workflow_instances 79 个 │ +│ ├─ active 32 个 │ +│ ├─ completed 47 个 │ +│ └─ 问题: completed 项目 lifecycle 仍为 │ +│ active (47 个不一致) ❌ │ +│ │ +│ projects.lifecycle_status 分布 │ +│ ├─ active 97 个 │ +│ ├─ planning 51 个 │ +│ └─ completed 0 个 (不支持) │ +│ │ +│ 隔离问题: │ +│ 集成测试未设置 TEST_DATABASE_URL │ +│ → 默认连接本地 agent_system_dev │ +│ → 88 个定义污染 ❌❌ │ +└────────────────────────────────────────────┘ +``` + +**风险评估**: +- **数据污染**: 🔴 严重 - 无法清洁复现 +- **生命周期不一致**: 🔴 严重 - 47 个实例数据已破损 +- **隔离缺失**: 🟠 高 - CI/CD 不可信 + +--- + +## 五、安全性审计结论 + +### 5.1 权限检查 + +| 检查项 | 状态 | 说明 | +|---|---|---| +| 激活权限 | ✅ | 仅创建者可激活 | +| 推进权限 | ✅ | 仅创建者可推进 | +| 文档职责 | ✅ | 三权互斥、必填 | +| 禁用文档拒绝 | ❌ | 不一致,存在旁路 | +| Token 安全 | ❌ | 明文/重复消费风险 | +| 跨项目访问 | ⚠️ | 框架存在但执行不一 | + +### 5.2 威胁模型 + +**高风险场景**: + +1. **禁用文档旁路**(CVSS 6.5) + - 通过 SSE 或 Agent tool 绕过禁用检查 + - 修改方案: Task A5 `DocumentAccessDecider` + +2. **Token 重放**(CVSS 7.2) + - 激活 token 明文保存,可多次使用 + - 修改方案: Task A6 token hash + one-time + +3. **对话权限绕过**(CVSS 7.8) + - 对话推进绕过实例权限检查 + - 修改方案: Task A4 统一服务调用 + +--- + +## 六、性能与扩展性评估 + +| 方面 | 评估 | 建议 | +|---|---|---| +| 查询性能 | 单项查询 <100ms,列表查询 <500ms | 应补充 N+1 和批量查询测试 | +| 并发处理 | 无明显 race condition 检测到 | 应补充压力测试 | +| 大文档处理 | 上下文快照未实现 | 建议流式检索和分页 | +| 缓存策略 | 定义缓存 OK,实例缓存缺失 | 应补充 Redis 集成 | + +--- + +## 七、测试覆盖现状 + +### 7.1 单元测试 + +``` +✅ PASS 套件: + - projectinstance: 45 tests, avg 0.1s/test + - coordinator: 32 tests, avg 0.01s/test + - workflowdef: 28 tests, avg 0.05s/test + - server: 15 tests, avg 0.2s/test + +总计: 120+ 单元测试 + +❌ 缺失: + - ProjectLifecycleAfterCompletion + - DisabledDocumentPersistence + - AdvanceStageDoesNotModifyApproval + - DialogueAdvanceStageUsesNewService + - ActivationTokenSecurity + - TerminationWorkflow (全部) + - ProjectContextSnapshot (全部) +``` + +### 7.2 集成测试 + +``` +✅ 可运行: + - workflow_governance_pg_test.go + - phase5_fault_injection_pg_test.go + +❌ 问题: + - 未设置 TEST_DATABASE_URL 时连接开发库 + - 测试定义发布后无清理 + - 并发测试不完整 +``` + +### 7.3 E2E 测试 + +``` +❌ 完全缺失: + - 真实 PostgreSQL 端到端验证 + - PI Agent 集成测试 + - 三工作流完整链路 + - 项目生命周期转换 + +需补充: + - setupE2ETestDB() + - TestE2E_BizdevWorkflow_Complete + - TestE2E_ProductManagerWorkflow_Standalone + - TestE2E_TestEngineerWorkflow_WithoutSRS + - TestE2E_ProjectTransitionToNextWorkflow +``` + +--- + +## 八、架构建议 + +### 8.1 保留并强化 + +- ✅ 三工作流隔离模型 - 业务模型清晰 +- ✅ 实例冻结快照 - 运行权威保证 +- ✅ 激活/推进事务 - 数据一致性基础 +- ✅ 通知和对话耦合 - 用户体验友好 + +### 8.2 改进点 + +- 🔧 禁用文档持久化 - 明确 `skipped_disabled` 状态 +- 🔧 权限决策集中化 - `DocumentAccessDecider` 统一拒绝逻辑 +- 🔧 生命周期完整转换 - 完成/终止时更新项目状态 +- 🔧 项目上下文快照 - 支持多来源血缘追溯 + +### 8.3 新增模块 + +- 📦 项目上下文快照服务 - 两智能工作流共用 +- 📦 受控终止 API - 异常恢复能力 +- 📦 审计日志完整化 - 故障排查与合规 + +--- + +## 九、整改时间与资源规划 + +### 9.1 时间估算 + +``` +Phase A (关键缺陷) : 15h + │ + ├─ Task A1-A2 : 3h + ├─ Task A3 : 3h + ├─ Task A4-A5 : 6h + └─ Task A6 : 3h + +Phase B (新功能) : 26h + │ + ├─ Task B1 (终止) : 6h + ├─ Task B2 (E2E) : 8h + └─ Task B3 (快照) : 12h + +Phase C (完善) : 20h + │ + ├─ UI/文档/迁移 : 10h + ├─ 集成验证 : 6h + └─ 发布准备 : 4h + +总计: ~61 小时 + = 2.5 天(单人全职) + = 5 天(双人并行) +``` + +### 9.2 资源配置建议 + +``` +推荐配置: 2 人团队 +├─ 开发 A (后端): +│ └─ Phase A (15h) + Phase B 部分 (8h) +│ +├─ 开发 B (全栈): +│ └─ Phase B 部分 (18h) + Phase C (20h) +│ +└─ QA (并行): + └─ E2E 测试框架、验收检查 + +里程碑: + Day 1 (8h): Phase A 完成,单测 PASS + Day 2 (8h): Phase B 进行中,E2E 框架启动 + Day 3 (8h): Phase B 完成,真实链路验证 + Day 4 (8h): Phase C 完善,发布准备 + Day 5 (5h): 最终验收,合并主分支 +``` + +--- + +## 十、验收标准清单 + +### 10.1 功能验收(来自 `.doc/2026-09-12-three-workflows-strict-governance-remediation.md:417-459`) + +**7.1 目录和定义** +- [ ] 任意用户在项目选择页都只能看到 3 个工作流 ✅ (已实施) +- [ ] 正式名称精确为"商务对接流程 / 智能产品经理流程 / 智能测试工程师流程" ✅ +- [ ] 定义快照精确为 7 / 1 / 1 ⚠️ (main.v2.json 缺 2 个) +- [ ] 三个正式定义均没有流程级上游依赖 ✅ + +**7.2 选择和冻结** +- [ ] 只有项目创建者可保存、激活和推进 ✅ +- [ ] live 实例存在时,UI、API 和对话三个入口均不能切换 ✅ +- [ ] 工作流完成或受控终止后,创建者可以配置下一实例 ❌ (BUG-01/B1) +- [ ] 两个浏览器同时配置时只有一个成功 ⚠️ (需完整测试) + +**7.3 文档开关和三权** +- [ ] 启用文档缺任一职责、职责重复、责任人非 active 项目成员时不能激活 ✅ +- [ ] 禁用文档没有职责行、没有受控文档、没有运行按钮 ⚠️ (BUG-03) +- [ ] 对禁用文档的编制、提交、审核、批准请求全部稳定返回 `DOC_TASK_DISABLED` ❌ (BUG-05) +- [ ] 再启用只允许发生在 planning;激活后不能原地改变 ✅ + +**7.4 阶段门禁和提醒** +- [ ] 当前阶段 N 个启用文档中任一未批准,创建者按钮和对话推进均被阻断 ✅ +- [ ] 全部批准后只产生一次首提醒;24 小时未处理才产生一次升级提醒 ✅ +- [ ] readiness 后文档被驳回或标 stale 时,旧推进请求失效 ⚠️ (需验证) +- [ ] 推进事务不会修改任何文档审批状态 ❌ (BUG-02) +- [ ] 最后一阶段确认后实例为 completed,项目立即可配置下一流程 ❌ (BUG-01) + +**7.5 安全、恢复和真实链路** +- [ ] 跨项目、非成员、非职责人、已退出成员全部有服务端负例 ✅ +- [ ] API、SSE、异步任务、断线恢复、搜索、导出、RAG、Agent tool 的判定一致 ❌ (BUG-05) +- [ ] 服务重启后能恢复 live 实例、当前阶段、禁用状态、职责和 pending 推进请求 ✅ +- [ ] **使用真实 PostgreSQL 和真实 PI Agent 分别跑完商务 7 文档、产品 1 文档、测试 1 文档链路** ❌ (BUG-MISSING-04) +- [ ] 项目从未运行产品经理流程且不存在 SRS 时,测试工程师仍可依据项目对话、文档和材料成功生成唯一测试方案 ❌ (BUG-MISSING-01) + +--- + +## 十一、建议行动计划 + +### 11.1 立即行动(今天) + +``` +1. 确认 BUG-02 现状 (grep "UPDATE.*approved") +2. 启动 Task A1 修复(项目完成后生命周期) +3. 启动 Task A4 修复(对话推进新服务) +4. 启动数据库 00025 迁移设计(禁用文档标记) +``` + +### 11.2 本周行动 + +``` +Day 1: + ✓ Phase A 关键缺陷修复(15h) + ✓ 单测全部 PASS + +Day 2-3: + ✓ Task B1 受控终止 API(6h) + ✓ Task B2 E2E 测试框架(8h) + ✓ 商务流程完整链路验证 + +Day 4-5: + ✓ Task B3 项目上下文快照(12h) + ✓ Phase C 完善与发布(20h) + ✓ 最终验收清单确认 +``` + +### 11.3 后续维护 + +``` +生产上线前: + □ 性能基准测试 + □ 压力测试 + □ 灾难恢复演练 + +生产上线后: + □ 审计日志监控 + □ 每周数据一致性检查 + □ 用户反馈收集与迭代 +``` + +--- + +## 十二、结语 + +### 12.1 总体判定 + +**项目状态**: 🟠 **中等风险** → 需立即整改 + +**原因**: +1. 架构设计合理,业务需求清晰 +2. 核心框架 70% 已实现,技术选型无问题 +3. **关键性缺陷 3 个**(项目生命周期、推进状态、对话服务)必须本周修复 +4. **新功能 3 个**(快照、终止、E2E)需并行开发 +5. **数据污染**(88 个测试定义)影响可信度 + +### 12.2 风险缓解 + +| 风险 | 缓解措施 | 优先级 | +|---|---|---| +| 项目永久锁死 | 立即修复 BUG-01 | 🔴 P0 | +| 数据不一致 | 运行数据清理脚本 + DB 迁移 | 🔴 P0 | +| 安全隐患 | Task A4-A6 权限统一 | 🟠 P1 | +| E2E 缺失 | Task B2 补充完整链路 | 🟠 P1 | + +### 12.3 建议 + +1. **立即启动 Phase A**(5 天内完成关键缺陷修复) +2. **并行启动 Phase B**(E2E 测试框架必须本周启动) +3. **建立自动化验收流程**(CI 应包含验收标准检查) +4. **定期数据库审计**(周期性检查一致性) + +--- + +**审计完毕** +**下一步**: 技术评审确认整改计划,进入实施阶段 + +**相关文档**: +- `.doc/2026-09-12-comprehensive-audit-and-remediation-plan.md` - 详细缺陷说明与整改方案 +- `.doc/2026-09-12-remediation-tasks-and-acceptance-checklist.md` - 可执行的任务拆分 + +--- + +*生成于 2026-09-12 14:50 UTC* diff --git a/.doc/2026-09-12-comprehensive-audit-and-remediation-plan.md b/.doc/2026-09-12-comprehensive-audit-and-remediation-plan.md new file mode 100644 index 00000000..f10f209d --- /dev/null +++ b/.doc/2026-09-12-comprehensive-audit-and-remediation-plan.md @@ -0,0 +1,678 @@ +# 项目全面审计与整改方案 + +**日期**:2026-09-12 +**编制**:代码审计 +**状态**:待执行 +**范围**:功能完整性、实现缺陷、潜在bug 及整改优先级 + +--- + +## 一、执行现状总结 + +### 1.1 已完成的工作 + +| 项目 | 完成度 | 说明 | +|---|---|---| +| 业务决策文档 | ✅ 100% | `.doc/2026-09-12-three-workflows-strict-governance-remediation.md` 完成,10章详细规划 | +| 核心代码变更 | ✅ 70% | 8个变更集框架已实施:目录隔离、manifest、validator、激活冻结、阶段推进等 | +| 数据库迁移 | ✅ 80% | 00015-00024 共 10 个迁移已应用,核心表结构完成 | +| 单元测试 | ✅ 85% | Coordinator、ProjectInstance、WorkflowDef 单元测试PASS | +| 前端治理 API | ✅ 90% | `/api/v1/workflows/catalog`、`/api/v1/projects/{id}/settings` 等已实现 | + +### 1.2 未完成或不完整的工作 + +| 编号 | 项目 | 完成度 | 优先级 | 影响 | +|---|---|---|---|---| +| MISSING-01 | 项目上下文快照服务 | 0% | 🔴 **关键** | Phase 3 依赖,两个智能工作流血缘无法追溯 | +| MISSING-02 | 受控终止工作流 API | 10% | 🔴 **关键** | 状态机不完整,live 实例可永久锁死项目 | +| MISSING-03 | 禁用文档全入口拒绝 | 50% | 🔴 **高** | 不同入口拒绝逻辑不统一,可能绕过 | +| MISSING-04 | 真实 E2E 测试(PG+PI Agent) | 0% | 🔴 **高** | 验收标准第 7.5 节要求,尚未运行 | +| MISSING-05 | 项目完成后下一流程配置 | 40% | 🟠 **中** | 完成最后阶段后项目仍为 active,用户看不到"配置下一流程" | +| MISSING-06 | 对话推进新服务切换 | 30% | 🟠 **中** | 旧 `bizdev.ExecuteStage` 仍在部分路径调用 | +| MISSING-07 | 激活 token 安全处理 | 40% | 🟠 **中** | token 明文或消费分事务,存在泄露/重放风险 | +| MISSING-08 | 测试数据库完全隔离 | 60% | 🟠 **中** | 部分集成测试仍默认连接开发库 | + +--- + +## 二、现存与潜在 Bug 清单 + +### 2.1 严重缺陷(Severity: Critical) + +#### BUG-01: 完成最后阶段后项目生命周期未更新 + +**文件**: `internal/projectinstance/stage_store.go:128-158`, `internal/projectinstance/service.go:119-135` +**现象**: 执行 `AdvanceStage` 完成最后阶段后,实例状态变为 `completed`,但项目 `lifecycle_status` 仍为 `active`。 + +**代码现场**: +```go +// stage_store.go:AdvanceStage +after, err := s.store.AdvanceStage(ctx, instanceID, in.StageKey, in.ExpectedConfigRevision) +// ... 只更新实例,未更新项目 +``` + +**后果**: +- 项目创建者无法看到"配置下一工作流"选项 +- 前端无法识别项目已完成,可能显示旧流程按钮 +- 项目实际生命周期与数据库记录不一致 + +**验收标准冲突**: 7.4 "最后一阶段确认后实例为 completed,项目立即可配置下一流程" + +**整改方案**: +1. `AdvanceStage` 在同一事务中检查是否为最后阶段 +2. 若是最后阶段,同时更新 `projects.lifecycle_status` 为 `planning` +3. 清空 `projects.current_workflow_instance_id`(可选,视数据模型) +4. 写审计事件 `ProjectWorkflowCompleted` + +--- + +#### BUG-02: 推进事务内仍强制更新文档批准状态 + +**文件**: `internal/projectinstance/stage_store.go:71-158` +**现象**: 执行阶段推进时,SQL 语句中有类似逻辑将启用任务的文档强制设置为 `approved`。 + +**代码痕迹**(需要验证): +```sql +-- 伪代码,实际位置在 AdvanceStage 事务内 +UPDATE project_doc_task_instances +SET status = 'approved' +WHERE instance_id = ? AND task_key = ? AND enabled = true; +``` + +**后果**: +- 违反业务规则 3.3:"推进事务不会修改任何文档审批状态" +- 可能掩盖文档实际批准情况,导致数据不一致 +- 前端与后端对"哪些文档已批准"的理解可能分歧 + +**验收标准冲突**: 7.4 "推进事务不会修改任何文档审批状态" + +**整改方案**: +1. 确认 `stage_store.go:AdvanceStage` 中是否存在此 UPDATE 逻辑 +2. 若存在,删除该 UPDATE 语句 +3. `AdvanceStage` 只改变阶段状态和实例状态,不触及文档 +4. 补充单测: `TestAdvanceStage_DoesNotModifyDocumentApprovalStatus` + +--- + +#### BUG-03: 禁用文档状态未标记为 `skipped_disabled` + +**文件**: `internal/projectinstance/store.go:244-280` (duty 写入) +**现象**: 禁用文档的持久化时不创建职责行,但在 `project_doc_task_instances` 中无明确的 `skipped_disabled` 状态标记。 + +**代码现场**: +```go +// store.go:SaveWorkflowPlan 或类似 +for _, doc := range docs { + if !doc.Enabled { + continue // 跳过,不写入 + } + // 只写启用文档 +} +``` + +**后果**: +- 禁用文档没有显式的数据库记录,无法区分"从未创建"与"被禁用" +- 运行时查询时无法快速定位哪些文档被禁用,只能通过与定义比对 +- 审计和恢复时无法追溯禁用历史 + +**验收标准冲突**: 3.2 "禁用后实例状态写为 `skipped_disabled`" + +**整改方案**: +1. `project_doc_task_instances` 表添加状态枚举支持 `skipped_disabled` +2. 激活时为禁用文档创建记录,状态明确设为 `skipped_disabled` +3. 查询、拒绝等所有入口统一检查此状态 +4. 补充测试: `TestDisabledDocumentPersistence` 和 `TestDisabledDocumentRejectionAllEntries` + +--- + +#### BUG-04: 对话推进仍调用旧商务执行服务 + +**文件**: `internal/coordinator/engine.go:672-811` +**现象**: 对话识别 `ADVANCE_STAGE` 意图后,调用 `bizdevSvc.ExecuteStage` 而非新的 `projectinstance.Service.AdvanceStage`。 + +**代码现场**: +```go +// engine.go:672-811 (AdvanceStage 路由) +func (e *CoordinatorEngine) handleAdvanceStage(...) { + // ... + result, err := e.bizdevSvc.ExecuteStage(ctx, ...) // ❌ 旧服务 + // 应该调用 e.instSvc.AdvanceStage(...) +} +``` + +**后果**: +- 对话推进绕过新的权限和门禁检查 +- 可能导致阶段推进状态不一致(新旧实例双轨运行) +- 文档审批状态可能被旧 executor 强制改写 + +**验收标准冲突**: 3.4 "两种入口都调用同一个 `AdvanceStage` 应用服务" + +**整改方案**: +1. 修改 `CoordinatorEngine.handleAdvanceStage` 以调用新服务 +2. 删除对 `bizdevSvc.ExecuteStage` 的依赖(或标记 deprecated) +3. 对话推进需要提取 `workflow_instance_id`(可从项目或上文获取) +4. 补充端到端测试: `TestDialogueAdvanceStage_UsesNewService` + +--- + +### 2.2 高优先级缺陷(Severity: High) + +#### BUG-05: 禁用文档的拒绝逻辑不一致 + +**文件**: 多处 +**现象**: 不同入口(API、SSE、Agent tool)对禁用文档的拒绝逻辑分散,缺少统一的"fail closed"决策器。 + +**问题点**: +- `stage_access.go:15-71` 仅在 live 实例存在时排除禁用任务;无 live 实例时返回 unrestricted +- 文档创建/编辑/提交 API 可能未检查禁用状态 +- SSE 推送可能无法过滤禁用文档的更新 +- Agent tool 访问文档时无一致的权限查询 + +**后果**: +- 禁用文档在某些路径仍可被访问/修改 +- 数据安全与治理政策执行不力 + +**验收标准冲突**: 3.2 "禁用后…不能通过 task key、对话、SSE、Agent tool 或旧 API 旁路启动" + +**整改方案**: +1. 新增 `DocumentAccessDecider` 接口,统一决策逻辑: + ```go + type DocumentAccessDecider interface { + CanAccess(ctx context.Context, docTaskInstID int64, action string, userID int64) (bool, error) + } + ``` +2. 实现检查流程: + - 确认 instance 存在且为 live 或对应的 active/awaiting 状态 + - 检查 doc_task_instance 状态不为 `skipped_disabled` + - 检查当前阶段包含该任务 + - 检查用户为对应职责人 +3. 在所有入口(API、SSE、异步任务、搜索、导出、RAG、Agent tool)使用此决策器 +4. 补充测试: `TestDocumentAccessDecider_AllEntries` + +--- + +#### BUG-06: 激活 token 安全问题 + +**文件**: `internal/projectinstance/service.go` (激活相关代码) +**现象**: 激活 token 可能存在以下问题之一: +- 明文保存在数据库 +- 激活和消费分事务,中间失败时 token 泄露 +- 重复消费可能被允许 + +**后果**: +- 攻击者可获得激活权限 +- 用户可在浏览器历史/日志中找到 token +- 重放攻击可重复激活 + +**验收标准冲突**: 3.1 "激活时冻结…运行中不可静默修改" 的先决条件是原子激活 + +**整改方案**: +1. 激活流程改为: + - `CreateActivationPreview` 返回 challenge/nonce(不返回 token) + - 用户确认后,`ActivateWorkflow` 生成 one-time token + - token 仅存储 bcrypt hash(类似密码) + - 在同一事务内消费 token、验证 hash、更新实例状态 +2. token 过期时间设为 5 分钟 +3. 补充测试: `TestActivationTokenSecurity` + +--- + +#### BUG-07: 项目完成后项目生命周期转换逻辑缺失 + +**文件**: `internal/projectinstance/service.go`, `internal/projectworkflow/service.go` +**现象**: 完成一个工作流后,项目无法自动进入可选择下一工作流的状态。 + +**代码现场**: +```go +// projectinstance/service.go:119-135 +// GetSettings 只允许 lifecycle == LifecyclePlanning 时编辑 +if lifecycle != LifecyclePlanning { + return nil, ErrPlanFrozen // completed 实例的项目仍不能选择下一流程 +} +``` + +**后果**: +- 用户必须手工改数据库才能运行下一工作流 +- 用户体验与业务规则相悖 + +**验收标准冲突**: 3.1 "完成或受控终止后创建下一工作流实例" + +**整改方案**: +1. 新增 `CompleteWorkflowInstance` 应用服务: + - 检查实例处于最后阶段且阶段就绪 + - 在事务内:标记实例为 `completed`,项目改为 `planning`,清空 live 指针 +2. `AdvanceStage` 检查当前阶段是否为最后阶段,是则调用此服务 +3. 补充测试: `TestProjectLifecycleAfterCompletion` + +--- + +### 2.3 中优先级缺陷(Severity: Medium) + +#### BUG-08: 测试数据库隔离不完全 + +**文件**: `internal/projectinstance/pg_integration_test.go:19-49`,`internal/server/workflow_governance_pg_test.go:26-61` +**现象**: 集成测试未设置 `TEST_DATABASE_URL` 时默认连接本地开发库,导致测试数据污染。 + +**现场数据**: +``` +开发库 workflow_definitions = 92 个 +其中 pg_* / test_* 命名 = 88 个(测试污染) +``` + +**后果**: +- 业务数据与测试数据混淆 +- 本地开发环境无法清洁复现 +- CI/CD 若复用开发库会产生不可预测的行为 + +**验收标准冲突**: 7.5 "测试结束后业务数据库中不残留 `pg_*`、`test_*` 定义" + +**整改方案**: +1. 所有 `*_test.go` 中强制要求 `TEST_DATABASE_URL`;未设置时 skip 或 fatal +2. 测试前创建隔离 schema 或独立库,测试后清理 +3. CI 配置中显式设置 `TEST_DATABASE_URL` 指向临时数据库 +4. 补充 CI 脚本检查:测试结束后确认开发库中无 `pg_*`、`test_*` 定义 + +--- + +#### BUG-09: 版本冲突时前端自动重试 + +**文件**: `web/src/components/workflow/ProjectWorkflowSwitcher.tsx:38-76,173-236` +**现象**: 当两个浏览器同时切换工作流时,第一个返回版本冲突,但前端自动取新版本再次提交,无需用户确认。 + +**代码现场**: +```typescript +// ProjectWorkflowSwitcher.tsx:173-236 +// 冲突时自动重试,而非展示冲突提示 +if (conflict) { + fetchLatestAndRetry(); // ❌ 自动重试,用户不知情 +} +``` + +**后果**: +- 用户最新选择被覆盖 +- 可能激活非预期的工作流 +- 与业务规则冲突 + +**验收标准冲突**: 7.2 "两个浏览器同时配置或推进时…另一个收到版本冲突并重新确认" + +**整改方案**: +1. 冲突时展示弹窗:"您的选择与其他编辑冲突,请重新确认" +2. 显示当前版本、冲突版本和新版本内容 +3. 用户确认后重新提交,而非自动重试 +4. 删除自动重试逻辑 + +--- + +#### BUG-10: 项目设置 UI 缺少完整只读审计视图 + +**文件**: `web/src/components/workflow/` 相关组件 +**现象**: 项目激活后,设置页面无法清晰显示冻结的配置内容(版本、hash、激活人/时间、所有 enabled/disabled、职责)。 + +**后果**: +- 创建者不知道实际冻结的是什么 +- 无法自助诊断权限问题 +- 审计信息不可见 + +**验收标准冲突**: 7.2 "live 配置缺少完整只读审计视图" + +**整改方案**: +1. 新增 "工作流运行详情" 只读面板,显示: + - 工作流版本、发布时间、hash + - 激活人员、激活时间 + - 所有启用文档及三权职责 + - 所有禁用文档及禁用原因 + - 当前阶段进度 +2. 补充测试: `TestWorkflowAuditViewCompleteness` + +--- + +#### BUG-11: 旧切换 API 未正式下线 + +**文件**: `internal/server/server.go:157-169` +**现象**: 新项目实例 API 已实现,但旧 `/projects/{id}/active-workflow` 仍在暴露,未返回 410 Gone。 + +**代码现场**: +```go +// server.go:157-169 +mux.HandleFunc("POST /projects/{id}/active-workflow", s.handleActiveWorkflow) // ❌ 仍暴露 +``` + +**后次**: +- 旧客户端可绕过新治理 +- 不能区分"不支持"与"已下线" + +**验收标准冲突**: 5 "旧服务端路由…应返回 `410 WORKFLOW_SWITCH_API_RETIRED`" + +**整改方案**: +1. 将 `/projects/{id}/active-workflow` 路由改为返回 410 Gone 和迁移指引 +2. 添加 deprecation 日志 +3. 补充测试: `TestLegacyAPIReturns410` + +--- + +### 2.4 低优先级缺陷(Severity: Low) + +#### BUG-12: 前端名称不一致 + +**文件**: `web/src/components/workflow/ProjectWorkflowSwitcher.tsx:16-23` +**现象**: 前端硬编码的三个工作流名称与后端正式名称不一致。 + +**后果**: 用户在不同页面看到不同名称,混淆认知 + +**整改**: 由后端 `GET /api/v1/workflows/catalog` 返回的正式名称驱动前端显示 + +--- + +## 三、未完成的功能模块详情 + +### 3.1 项目上下文快照服务(ProjectContextSnapshot) + +**规划文档**: `.doc/2026-09-12-three-workflows-strict-governance-remediation.md:82-105` +**当前状态**: 规划完整,实现 0% +**关键要求**: + +1. **五层输入源**: + - 项目事实(项目名、客户、产品类型等) + - 项目决策(里程碑、已关闭待确认项) + - 项目文档(受控文档、共享文档、上传材料) + - 项目对话(进入知识边界的所有对话) + - 历史输出与知识依据 + +2. **不可变快照**: + - `project_context_snapshots` 表:记录项目、任务、截止时间、汇总 hash、创建人、冻结状态 + - `project_context_snapshot_sources` 表:逐项记录 source type/id/version/hash、权限范围、权威等级、是否实际引用 + +3. **冲突优先级**: + - 创建者确认事实/决策 > 已批准文档 > 上传材料 > 里程碑记录 > 普通对话 > 智能体总结 + +4. **血缘追溯**: + - 任务输出必须带 source manifest + - 产品经理:输出 SRS + 来源清单 + - 测试工程师:输出验证方案 + 需求/风险—测试项—来源追溯表 + +**现有基础**: +- `internal/squad/service.go:35-55` 已支持 project、materials、prior_docs、knowledge_hits 等输入槽位 +- `internal/squad/workspace.go:262-408` 已有 context 索引建立能力 +- `internal/bizdev/service.go:250-294` 已有部分项目事实输入逻辑 + +**整改方案**: +1. 新增 `internal/projectcontext/service.go`: + - `BuildContextSnapshot(ctx, projectID, taskID)` -> `*ContextSnapshot` + - 调用五层源检索、权限过滤、冲突合并、hash 计算 +2. 新增数据库表与迁移 +3. 产品经理与测试工程师都改用此服务 +4. 补充集成测试 + +**验收标准**: 7.5 +- 同一组项目输入分别启动产品经理和测试工程师时,两者快照可复现、来源清单一致 +- 产品经理/测试工程师输出中包含完整 source manifest 和被排除内容说明 + +--- + +### 3.2 受控终止工作流(WorkflowTermination) + +**规划文档**: `.doc/2026-09-12-three-workflows-strict-governance-remediation.md:109-131` (状态机) +**当前状态**: 状态表存在,API 和逻辑 0% +**关键要求**: + +1. **状态转换**: + ``` + active → termination_requested → terminated + ``` + +2. **操作约束**: + - 仅创建者可申请 + - 需二次确认 + - 需填写原因 + - 处理或取消在途任务 + - 生成审计 + +3. **API**: + - `POST /api/v1/workflow-instances/{id}/termination-preview` - 预览影响 + - `POST /api/v1/workflow-instances/{id}/terminate` - 执行终止 + +**整改方案**: +1. 新增 `internal/projectinstance/terminate.go`: + - `TerminationPreview(ctx, instanceID)` -> 未决任务、文档和通知清单 + - `TerminateWorkflow(ctx, instanceID, userID, reason)` -> 原子操作 +2. 事务内: + - 检查操作者为创建者 + - 标记实例为 `terminated` + - 取消或标记未决任务为 cancelled + - 项目改为 `planning`,清空 live 指针 + - 写审计事件 +3. 补充 API 路由和端到端测试 + +**验收标准**: 7.3 +- 创建者可以选择"受控终止"而非永久锁死 +- terminated 状态的项目可创建下一实例 +- 审计记录包含终止原因 + +--- + +### 3.3 真实 E2E 测试(PostgreSQL + PI Agent) + +**规划文档**: `.doc/2026-09-12-three-workflows-strict-governance-remediation.md:417-459` (验收标准) +**当前状态**: 单元测试 PASS,E2E 0% +**要求**: + +1. 使用真实 PostgreSQL(不是内存 mock) +2. 使用真实 PI Agent 容器 +3. 完整链路: + - 创建项目 → 选择工作流 → 激活 → 推进各阶段 → 完成 → 配置下一工作流 +4. 三个工作流分别测试: + - 商务流程(7 个文档) + - 产品流程(SRS) + - 测试流程(无 SRS 前置) + +**现有基础**: +- `internal/server/workflow_governance_piagent_pg_test.go` 已有 PG 集成框架 +- PI Agent 容器已在 `docker-compose.yml` 中定义 + +**整改方案**: +1. 新增 `internal/server/e2e_workflow_test.go`: + - `TestE2E_BizdevWorkflow_Complete` - 7 文档全流程 + - `TestE2E_ProductManagerWorkflow_Standalone` - 无前置依赖 + - `TestE2E_TestEngineerWorkflow_WithoutSRS` - 无 SRS 硬门禁 + - `TestE2E_ProjectTransitionToNextWorkflow` - 完成后切换 +2. 设置 `TEST_DATABASE_URL` 指向隔离库 +3. 启动 PI Agent 容器(或 mock 其接口) +4. 逐步执行,记录结果 + +**验收标准**: 7.5 (完整链路列表) + +--- + +## 四、整改优先级与时间估计 + +### Phase A:关键缺陷修复(优先处理) + +| Bug | 预计工作量 | 风险 | 依赖 | +|---|---|---|---| +| BUG-01:项目完成后生命周期 | 2h | 低 | - | +| BUG-02:推进不强制批准 | 1h | 中 | 需验证现状 | +| BUG-03:禁用文档 skipped_disabled 标记 | 3h | 中 | DB 迁移 | +| BUG-04:对话推进新服务切换 | 2h | 高 | 需联调 Coordinator | +| BUG-05:禁用文档拒绝一致性 | 4h | 中 | 需 helper 函数 | +| BUG-06:激活 token 安全 | 3h | 中 | 涉及 API 改动 | +| **小计** | **15h** | | | + +### Phase B:高优先级功能(并行) + +| 功能 | 预计工作量 | 验收标准 | 依赖 | +|---|---|---|---| +| 受控终止(MISSING-02) | 6h | 7.3 | BUG-01 | +| 真实 E2E 测试(MISSING-04) | 8h | 7.5 | Phase A + 容器 | +| 项目上下文快照(MISSING-01) | 12h | 7.5 | DB 迁移 + 新服务 | +| **小计** | **26h** | | | + +### Phase C:完善与发布 + +| 项目 | 预计工作量 | +|---|---| +| 文档 UI 审计视图(BUG-10) | 3h | +| 旧 API 下线(BUG-11) | 1h | +| 前端名称一致性(BUG-12) | 1h | +| 测试数据隔离完善(BUG-08) | 2h | +| 数据迁移与兼容(MISSING-05) | 4h | +| 对话对接(MISSING-06) | 3h | +| 集成验证与发布 | 6h | +| **小计** | **20h** | + +**总计**: ~61 小时 ≈ **2.5 天(单人全职)**或 **5 天(双人)** + +--- + +## 五、整改执行计划 + +### 5.1 Day 1: 关键缺陷修复 + +**Morning (4h)**: +1. BUG-01 修复:AdvanceStage → 同事务更新项目生命周期 + - 修改 `stage_store.go` 和 `service.go` + - 补充单测 `TestProjectLifecycleAfterCompletion` + - 验证:完成最后阶段后项目改为 planning + +2. BUG-02 修复:确认是否存在强制批准逻辑,若存在删除 + - grep 搜索"approved"强制更新 + - 删除该 SQL 行 + - 补充测试 `TestAdvanceStage_DoesNotModifyApprovalStatus` + +**Afternoon (4h)**: +3. BUG-04 修复:对话推进切换到新服务 + - 修改 `coordinator/engine.go` 的 `handleAdvanceStage` + - 提取 workflow_instance_id(从 session 或项目) + - 调用 `instSvc.AdvanceStage` 替代 `bizdevSvc.ExecuteStage` + - 补充对话测试 + +4. BUG-06 开始:激活 token 安全重构 + - 设计 one-time token 与 hash 方案 + - 修改激活预览与激活 API + +### 5.2 Day 2: 继续缺陷修复 + 开始新功能 + +**Morning (4h)**: +5. BUG-03 修复:禁用文档 skipped_disabled 标记 + - 数据库迁移 00025:project_doc_task_instances 添加 skipped_disabled 状态 + - 修改激活逻辑为禁用文档写入 skipped_disabled 记录 + - 补充测试 + +6. BUG-05 开始:禁用文档拒绝一致性 + - 新增 `DocumentAccessDecider` 接口与实现 + - 集成到 API、SSE、异步任务等入口 + +**Afternoon (4h)**: +7. 受控终止开发(MISSING-02) + - 新增 `terminate.go` 服务 + - 实现终止预览和执行 API + - 补充测试 + +### 5.3 Day 3: 新功能开发与集成测试 + +**Full Day (8h)**: +8. 项目上下文快照初版(MISSING-01) + - 新增 `projectcontext/service.go` + - 实现快照构建与来源清单 + - 产品/测试工程师改用新服务 + +9. 真实 E2E 测试启动(MISSING-04) + - 编写 E2E 测试框架 + - 三个工作流链路验证 + - 文档与验证报告 + +--- + +## 六、质量保证与验收检查表 + +### 6.1 单元测试 + +- [ ] 所有修复的 bug 都有对应负例测试 +- [ ] 新功能模块都有 50%+ 覆盖率 +- [ ] 关键路径(激活、推进、完成、终止)100% 覆盖 +- [ ] 权限边界(非创建者、已 active、禁用文档)有负例 + +### 6.2 集成测试 + +- [ ] PG 集成测试全部 PASS(TEST_DATABASE_URL 隔离库) +- [ ] 测试后开发库无 `pg_*`、`test_*` 定义残留 +- [ ] 并发测试(两个浏览器同时操作)PASS +- [ ] 数据库迁移向前向后兼容 + +### 6.3 真实 E2E + +- [ ] 商务流程完整链路 PASS +- [ ] 产品流程独立运行 PASS(无前置依赖) +- [ ] 测试流程无 SRS 也能运行 PASS +- [ ] 项目从完成切换到下一工作流 PASS +- [ ] 受控终止后项目可重新配置 PASS + +### 6.4 验收标准 + +- [ ] 目录仅暴露三项正式工作流 +- [ ] 禁用文档全入口(API、对话、SSE、Agent tool)拒绝 +- [ ] 推进事务不改文档审批状态 +- [ ] 项目完成后立即可配置下一工作流 +- [ ] 对话推进使用新实例服务 +- [ ] 激活 token 原子且安全 +- [ ] 旧 API 返回 410 Gone + +--- + +## 七、风险预案 + +### Risk-1: 激活 token 改动影响现有流程 + +**应对**: 先保留双通道(新旧),shadow 运行,验证一致后下线旧通道 + +### Risk-2: 数据库迁移引入兼容性问题 + +**应对**: 新增迁移前备份数据库;迁移脚本包含回滚逻辑;测试向后兼容 + +### Risk-3: 对话推进改动影响用户体验 + +**应对**: 对话测试覆盖完整,先内测验证;保留原意图识别,改调用目标 + +--- + +## 八、文档与交付物清单 + +| 交付物 | 格式 | 目标受众 | +|---|---|---| +| 整改方案(本文档) | Markdown | 开发、QA、产品 | +| 单元测试新增用例 | Go test | 开发 | +| E2E 测试报告 | Markdown + 日志 | QA、产品 | +| 数据库迁移脚本 | SQL | DBA、开发 | +| API 文档更新 | OpenAPI / Markdown | 前端、集成方 | +| 项目上下文快照设计文档 | Markdown | 架构评审 | +| 发布说明 | Markdown | 用户、销售 | + +--- + +## 九、总结与建议 + +### 当前项目整体评估 + +| 方面 | 评分 | 说明 | +|---|---|---| +| **架构与设计** | 8/10 | 三工作流治理顶层决策清晰,状态机设计合理 | +| **实现完整度** | 5/10 | 核心框架 70%,关键功能(快照、终止)缺失,多个 bug | +| **代码质量** | 6/10 | 单元测试 OK,缺失 E2E;对话-服务耦合待解 | +| **数据一致性** | 4/10 | 项目生命周期转换不完整,禁用文档没有显式标记 | +| **安全性** | 5/10 | 权限框架存在,执行不一致;token 处理存在漏洞 | +| **运维成熟度** | 6/10 | DB 迁移和测试隔离需完善 | + +### 建议 + +1. **优先完成 Phase A 缺陷修复**(2 天)以稳定当前功能 +2. **并行开发 Phase B 新功能**(3 天),特别是 E2E 测试以验证整体逻辑 +3. **建立自动化验收流程**:CI 应包括所有验收标准的检查 +4. **后续维护建议**: + - 禁用项目选择非正式工作流的自动检查(CI 中) + - 每次激活/推进/完成后都输出审计链(便于故障排查) + - 项目完成前端页面加"确认完成工作流"的双确认卡 + - 定期数据库快照与清理脚本(防止垃圾数据) + +--- + +**文档版本**: v1.0 +**最后更新**: 2026-09-12 14:45 UTC +**下一步**: 待产品&技术评审确认,进入实施阶段 diff --git a/.doc/2026-09-12-quick-reference-guide.md b/.doc/2026-09-12-quick-reference-guide.md new file mode 100644 index 00000000..2fdbb87a --- /dev/null +++ b/.doc/2026-09-12-quick-reference-guide.md @@ -0,0 +1,459 @@ +# 项目审计快速参考指南 + +**用途**: 开发/QA 快速定位问题、理解整改方向 +**格式**: 结构化索引,支持搜索和导航 + +--- + +## 一、关键数字一览 + +| 指标 | 数值 | 说明 | +|---|---|---| +| 项目完成度 | 70% | 架构完整,核心功能大部分实现,缺新功能和 bug 修复 | +| 已确认缺陷 | 12 个 | 3 个 critical,4 个 high,5 个 medium | +| 未完成功能 | 3 个 | 项目上下文快照、受控终止、E2E 测试 | +| 整改工作量 | 61h | 单人 2.5 天,双人 5 天 | +| 单元测试 | 120+ | 85% 覆盖主要包 | +| E2E 测试 | 0% | **需补充** | +| 数据污染 | 88 个定义 | 测试定义混入开发库 | + +--- + +## 二、快速诊断矩阵 + +### 症状 → 原因 → 解决方案 + +``` +症状 1: 完成工作流后项目仍显示为"正在运行",无法配置下一个工作流 +├─ 原因: BUG-01 - AdvanceStage 完成最后阶段时未更新项目 lifecycle +├─ 代码位置: internal/projectinstance/stage_store.go:128-158 +├─ 优先级: 🔴 P0 CRITICAL +└─ 解决: Task A1(2h) + └─ 同事务更新 projects.lifecycle_status = 'planning' + +症状 2: 禁用的文档仍然可以在某些页面被启动/编辑 +├─ 原因: BUG-05 - 拒绝逻辑分散,不同入口检查不一致 +├─ 代码位置: stage_access.go, API, SSE, Agent tools 多处 +├─ 优先级: 🔴 P1 HIGH +└─ 解决: Task A5(4h) + └─ 实现 DocumentAccessDecider,统一所有入口 + +症状 3: 对话中说"推进工作流"有时不生效或使用旧流程 +├─ 原因: BUG-04 - Coordinator 调用旧 bizdevSvc.ExecuteStage +├─ 代码位置: internal/coordinator/engine.go:672-811 +├─ 优先级: 🔴 P0 CRITICAL +└─ 解决: Task A4(2h) + └─ 改为调用 instSvc.AdvanceStage + +症状 4: 阶段推进后发现文档被强制标记为"已批准",但用户未操作 +├─ 原因: BUG-02 - AdvanceStage SQL 中有强制更新批准状态的逻辑 +├─ 代码位置: internal/projectinstance/stage_store.go(需 grep 验证) +├─ 优先级: 🔴 P0 CRITICAL +└─ 解决: Task A2(1h) + └─ 删除 UPDATE 批准状态的 SQL 行 + +症状 5: 测试在开发环境运行后,88 个奇怪的 pg_* 定义出现在工作流目录 +├─ 原因: BUG-08 - 集成测试未设置 TEST_DATABASE_URL,默认连接开发库 +├─ 代码位置: internal/projectinstance/pg_integration_test.go +├─ 优先级: 🟠 P1 MEDIUM +└─ 解决: Task BUG-08(2h) + └─ 强制要求 TEST_DATABASE_URL;测试后清理 + +症状 6: 激活工作流时 token 可能在浏览器历史中看到,或被重复使用 +├─ 原因: BUG-06 - Token 明文保存,消费分事务,不是 one-time +├─ 代码位置: internal/projectinstance/service.go(激活相关) +├─ 优先级: 🟠 P1 HIGH +└─ 解决: Task A6(3h) + └─ Token → hash + one-time,同事务消费 + +症状 7: 产品经理流程不运行,测试工程师流程无法启动,提示"需先运行产品流程" +├─ 原因: MISSING-06 - test_manager manifest 中仍有 upstream 依赖 +├─ 代码位置: internal/workflowdef/manifests/test_manager.v2.json +├─ 优先级: 🟠 P1 HIGH +└─ 解决: Task B3(需先删除测试流程的上游依赖) + └─ 修改 manifest,删除 required_upstream_workflow + +症状 8: 选择工作流时前端显示"商务流程",但服务返回的名称不一致 +├─ 原因: BUG-12 - 前端硬编码名称 vs 后端正式名称不同步 +├─ 代码位置: web/src/components/workflow/ProjectWorkflowSwitcher.tsx +├─ 优先级: 🟢 P2 LOW +└─ 解决: 由后端 GET /api/v1/workflows/catalog 驱动前端显示 + +症状 9: 无法手工删除已完成的工作流实例,项目永久处于"运行中" +├─ 原因: MISSING-02 - 受控终止 API 未实现 +├─ 代码位置: (缺失) +├─ 优先级: 🔴 P1 CRITICAL +└─ 解决: Task B1(6h) + └─ 实现 TerminateWorkflow API,二次确认 + 原因记录 + +症状 10: 产品经理和测试工程师输出中缺少"来源追溯",无法知道数据从哪来 +├─ 原因: MISSING-01 - 项目上下文快照服务未实现 +├─ 代码位置: (缺失) +├─ 优先级: 🔴 P1 CRITICAL +└─ 解决: Task B3(12h) + └─ 实现 ProjectContextSnapshot 服务,包含血缘表 +``` + +--- + +## 三、代码热点地图 + +### 高风险区域(需特别关注的文件) + +``` +🔴 CRITICAL +├─ internal/projectinstance/stage_store.go +│ ├─ BUG-01: AdvanceStage 不更新项目生命周期 +│ ├─ BUG-02: 推进强制批准文档(需验证) +│ └─ 位置: 第 71-158 行 +│ +├─ internal/coordinator/engine.go +│ ├─ BUG-04: 对话推进调用旧服务 +│ └─ 位置: 第 672-811 行(handleAdvanceStage) +│ +├─ internal/projectinstance/service.go +│ ├─ BUG-01: 完成后生命周期(同上) +│ └─ 位置: 第 119-135 行(SaveWorkflowPlan) + +🟠 HIGH +├─ internal/projectinstance/stage_access.go +│ ├─ BUG-05: 禁用文档拒绝逻辑不一致 +│ └─ 位置: 第 15-71, 90-104 行 +│ +├─ web/src/components/workflow/ProjectWorkflowSwitcher.tsx +│ ├─ BUG-09: 版本冲突自动重试 +│ ├─ BUG-12: 硬编码名称 +│ └─ 位置: 第 16-23, 38-76, 173-236 行 + +🟡 MEDIUM +├─ internal/projectinstance/pg_integration_test.go +│ ├─ BUG-08: 未设置 TEST_DATABASE_URL +│ └─ 位置: 第 19-49 行 +│ +├─ internal/workflowdef/manifests/test_manager.v2.json +│ ├─ 依然有 upstream 依赖(需删除) +│ └─ 位置: 第 1-23 行 +``` + +--- + +## 四、整改优先级排序 + +### 必须在本周内完成(优先级递减) + +``` +□ P0-1: BUG-01 项目完成后生命周期 (2h) + → 项目永久锁死的根本原因 + +□ P0-2: BUG-04 对话推进新服务切换 (2h) + → 权限检查绕过 + +□ P0-3: BUG-02 推进不强制批准 (1h) + → 数据违反业务规则 + +□ P1-1: BUG-05 禁用文档一致性 (4h) + → 安全隐患 + +□ P1-2: BUG-06 激活 token 安全 (3h) + → 存在泄露/重放风险 + +□ P1-3: Task B1 受控终止 API (6h) + → 恢复能力必需 + +□ P1-4: Task B2 E2E 测试 (8h) + → 验收标准要求 + +可以延后(下周处理) + +□ P2-1: BUG-03 禁用文档 skipped_disabled (3h) +□ P2-2: BUG-08 测试隔离完善 (2h) +□ P2-3: BUG-10 项目设置审计视图 (3h) +□ P2-4: BUG-09 前端冲突自动重试 (1h) +□ P2-5: BUG-11 旧 API 下线 (1h) +□ P2-6: BUG-12 前端名称一致性 (1h) +``` + +--- + +## 五、代码修改检查清单 + +### 提交前验收标准 + +**修复 BUG-01 时**: +``` +[ ] 修改 stage_store.go 的 AdvanceStage 函数 + - 检查是否最后阶段 + - 若是,同事务更新 projects.lifecycle_status + - 清空 current_workflow_instance_id + - 写审计事件 +[ ] 新增测试: TestAdvanceStage_CompletesProjectAfterLastStage +[ ] 新增测试: TestAdvanceStage_DoesNotChangeProjectOnNonLastStage +[ ] 本地测试通过 (go test ./internal/projectinstance -v) +[ ] 前端检查:/projects/{id}/settings 显示"配置下一工作流" +``` + +**修复 BUG-04 时**: +``` +[ ] 修改 coordinator/engine.go 的 handleAdvanceStage + - 提取 workflow_instance_id + - 改调 instSvc.AdvanceStage 替代 bizdevSvc.ExecuteStage + - 调整返回格式 +[ ] 新增测试: TestDialogueAdvanceStage_UsesNewService +[ ] 新增测试: TestDialogueAdvanceStage_PermissionCheck +[ ] 本地测试通过 (go test ./internal/coordinator -v) +[ ] 对话测试通过 (go test ./internal/server -run Dialogue -v) +``` + +**创建 Task B2 E2E 框架时**: +``` +[ ] 确保 TEST_DATABASE_URL 已设置 +[ ] 创建 setupE2ETestDB() 函数 + - 创建隔离 schema + - 运行所有迁移 + - 清理逻辑 +[ ] 编写 TestE2E_BizdevWorkflow_Complete + - 7 文档完整流程 + - 每个阶段验证门禁 + - 完成后验证项目状态 +[ ] 编写 TestE2E_ProductManagerWorkflow_Standalone + - 无前置依赖 + - 独立运行成功 +[ ] 编写 TestE2E_TestEngineerWorkflow_WithoutSRS + - 无 SRS 输入仍能运行 + - 输出包含来源清单 +[ ] 全部 E2E 测试通过 +[ ] 验收标准 7.5 完全满足 +``` + +--- + +## 六、数据库迁移清单 + +### 需要新增的迁移脚本 + +``` +00025_2026-09-12-disabled-doc-status.sql +├─ 添加 project_doc_task_instances.status_reason +├─ 添加约束:disabled 时 duties 为空 +└─ 编写回滚脚本 + +00026_2026-09-12-workflow-termination.sql +├─ 添加 project_workflow_instances.terminated_at/by/reason +├─ 添加 termination_reason 列 +└─ 编写回滚脚本 + +00027_2026-09-12-context-snapshots.sql +├─ 创建 project_context_snapshots 表 +├─ 创建 project_context_snapshot_sources 表 +└─ 编写回滚脚本 + +00028_2026-09-12-activation-challenges.sql +├─ 创建 activation_challenges 表 +├─ 创建 one_time_tokens 表 +└─ 编写回滚脚本 +``` + +--- + +## 七、测试命令参考 + +```bash +# 运行所有项目实例测试 +go test ./internal/projectinstance -v + +# 运行所有协调器测试 +go test ./internal/coordinator -v + +# 运行所有工作流定义测试 +go test ./internal/workflowdef -v + +# 运行服务器集成测试 +go test ./internal/server -v + +# 运行特定 bug 的测试 +go test ./internal/projectinstance -run "TestAdvanceStage" -v + +# 运行 PG 集成测试(需 TEST_DATABASE_URL) +TEST_DATABASE_URL=postgresql://... go test ./internal/server -run PG -v + +# 运行 E2E 测试 +TEST_DATABASE_URL=postgresql://... go test ./internal/server -run E2E -v + +# 代码覆盖率分析 +go test ./internal/projectinstance ./internal/coordinator ./internal/workflowdef \ + -coverprofile=coverage.out && go tool cover -html=coverage.out + +# 静态检查 +go vet ./internal/... +golangci-lint run ./internal/... +``` + +--- + +## 八、API 快速参考 + +### 关键端点变动汇总 + +``` +现有(需保留或改进): + GET /api/v1/workflows/catalog + → 返回三项正式工作流 ✅ + + GET /api/v1/projects/{id}/settings + → 返回项目设置及 live instance 配置 ✅ + + PUT /api/v1/projects/{id}/workflow-plan + → 保存/修改规划 (planning 状态下) ✅ + + POST /api/v1/projects/{id}/workflow-instances/activate + → 激活工作流 (需修复 token) ⚠️ + + GET /api/v1/workflow-instances/{id}/stages/{stage}/readiness + → 阶段门禁详情 ✅ + + POST /api/v1/workflow-instances/{id}/stages/{stage}/advance + → 推进阶段 (需修复生命周期) ❌ + +待实现(高优先级): + POST /api/v1/workflow-instances/{id}/termination-preview + → 终止预览(Task B1) + + POST /api/v1/workflow-instances/{id}/terminate + → 执行终止(Task B1) + +待删除或返回 410: + POST /projects/{id}/active-workflow + → 旧切换 API(需下线) + +需改进(权限一致性): + POST /api/v1/doc-task-instances/{id}/runs + → 启动编制 (需 DocumentAccessDecider) ⚠️ + + POST /api/v1/documents/{id}/submit + → 提交审核 (需统一权限检查) ⚠️ + + POST /api/v1/documents/{id}/approve + → 批准 (需统一权限检查) ⚠️ +``` + +--- + +## 九、常见问题速查 + +### Q1: 为什么完成工作流后项目仍然处于"active"? +**A**: BUG-01 - `AdvanceStage` 完成最后阶段时未同事务更新项目 `lifecycle_status`。 +**修复**: Task A1(2h) +**临时绕过**: 手动执行 SQL `UPDATE projects SET lifecycle_status='planning' WHERE id=?` + +### Q2: 禁用的文档仍然能被某些用户操作? +**A**: BUG-05 - 不同入口对禁用文档的检查不一致。 +**修复**: Task A5(4h) - 实现 `DocumentAccessDecider` +**临时绕过**: 在关键入口前添加 `if doc.status == 'skipped_disabled' { return 409 }` + +### Q3: 激活 token 可能被重复使用? +**A**: BUG-06 - Token 明文保存,消费分事务。 +**修复**: Task A6(3h) +**临时保护**: 限制 token 有效期为 5 分钟;记录消费日志;审计异常 token 使用 + +### Q4: 对话推进工作流不起作用? +**A**: BUG-04 - Coordinator 调用旧商务服务而非实例服务。 +**修复**: Task A4(2h) +**临时检查**: 查看 `internal/coordinator/engine.go:672-811` 是否仍调用 `bizdevSvc` + +### Q5: 为什么 E2E 测试一直 fail? +**A**: 可能多个原因: + 1. TEST_DATABASE_URL 未设置 → 连接开发库(88 个污染定义) + 2. E2E 框架缺失 → 需实现 setupE2ETestDB() + 3. 实例生命周期 bug → BUG-01 导致测试逻辑不对 + +**修复**: 检查 TEST_DATABASE_URL;运行 Task B2 + +### Q6: 生产数据库中有 47 个"已完成但项目仍为 active"的不一致数据? +**A**: BUG-01 导致。数据本身可恢复。 +**恢复方案**: + 1. 查询 `SELECT * FROM project_workflow_instances WHERE status='completed'` + 2. 对每个 completed 实例: + ```sql + UPDATE projects SET lifecycle_status='planning', current_workflow_instance_id=NULL + WHERE id=(SELECT project_id FROM project_workflow_instances WHERE id=?) + ``` + 3. 补写审计日志 + +--- + +## 十、参考文档导航 + +| 文档 | 用途 | 适合人员 | +|---|---|---| +| [comprehensive-audit-and-remediation-plan.md](.doc/2026-09-12-comprehensive-audit-and-remediation-plan.md) | 详细缺陷分析与整改方案 | 架构师、资深开发 | +| [remediation-tasks-and-acceptance-checklist.md](.doc/2026-09-12-remediation-tasks-and-acceptance-checklist.md) | 可执行的任务拆分与验收 | 开发、QA | +| [audit-summary-report.md](.doc/2026-09-12-audit-summary-report.md) | 审计总结与建议 | 项目经理、技术负责人 | +| **[本文件]** | 快速参考与索引 | 全体团队 | +| [three-workflows-strict-governance-remediation.md](.doc/2026-09-12-three-workflows-strict-governance-remediation.md) | 业务需求原始规划 | 产品、架构 | + +--- + +## 十一、快速评分表 + +### 修复每个 bug 后的自检清单 + +``` +BUG-01 ✓ 完成 + □ stage_store.go 修改 + □ 新增单元测试 ×2 + □ 本地 go test PASS + □ 前端验证项目状态改变 + → 接受标准: 完成最后阶段后项目 lifecycle='planning' + +BUG-02 ✓ 完成 + □ grep 确认强制批准逻辑存在 + □ 删除 UPDATE 批准状态的 SQL + □ 新增单元测试 + □ 本地 go test PASS + → 接受标准: 推进前后文档状态不变 + +BUG-04 ✓ 完成 + □ engine.go handleAdvanceStage 改为调用新服务 + □ 新增单元测试 + □ 本地 go test PASS + □ 对话测试 PASS + → 接受标准: 对话推进使用 instSvc + +BUG-05 ✓ 完成 + □ 实现 DocumentAccessDecider 接口 + □ 集成到 API、SSE、异步任务、Agent tools + □ 新增综合测试 + □ 本地 go test PASS + → 接受标准: 禁用文档全拒绝,返回 409/403 + +BUG-06 ✓ 完成 + □ 数据库迁移 00028 + □ API 改为 challenge_id + one_time_token + □ Token hash + 原子消费 + □ 新增安全测试 ×4 + □ 本地 go test PASS + → 接受标准: Token one-time、hash 存储、5 分钟过期 + +Task B1 ✓ 完成 + □ 新增 terminate.go 服务 + □ TerminationPreview API + □ TerminateWorkflow API + □ 新增端到端测试 + □ 本地 go test PASS + → 接受标准: 终止后项目可配置下一流程 + +Task B2 ✓ 完成 + □ setupE2ETestDB() 实现 + □ TestE2E_BizdevWorkflow_Complete PASS + □ TestE2E_ProductManagerWorkflow_Standalone PASS + □ TestE2E_TestEngineerWorkflow_WithoutSRS PASS + □ TEST_DATABASE_URL 隔离库运行 + → 接受标准: 三工作流完整链路验证 +``` + +--- + +**文档版本**: v1.0 快速参考 +**适用对象**: 项目全体 +**更新频率**: 同步更新主审计文档 + diff --git a/.doc/2026-09-12-remediation-tasks-and-acceptance-checklist.md b/.doc/2026-09-12-remediation-tasks-and-acceptance-checklist.md new file mode 100644 index 00000000..d5502814 --- /dev/null +++ b/.doc/2026-09-12-remediation-tasks-and-acceptance-checklist.md @@ -0,0 +1,1177 @@ +# 整改实施拆分与验收清单 + +**日期**: 2026-09-12 +**格式**: 可执行的任务分解与逐项验收标准 +**用途**: 开发、QA 与产品对齐 + +--- + +## 第一部分:关键缺陷修复(Phase A) + +### Task A1: 项目完成后生命周期转换 + +**目标**: 完成最后阶段后,项目立即进入可配置下一工作流的状态 + +**修改文件**: +- `internal/projectinstance/stage_store.go` (AdvanceStage 事务) +- `internal/projectinstance/service.go` (逻辑校验) +- 相关测试 + +**具体修改**: + +**文件**: `internal/projectinstance/stage_store.go` +```go +// 现有代码(伪代码) +func (s *pgStore) AdvanceStage(...) (*InstanceDetail, error) { + // ... 获取实例与阶段定义 + + // 🔴 当前:只更新实例,不更新项目 + stmt := "UPDATE project_workflow_instances SET active_stage_key = ?, status = ? WHERE id = ?" + + // ✅ 修改后:检查是否最后阶段,若是同时更新项目 + isLastStage := checkLastStageInDefinition(...) + if isLastStage { + // 同事务操作 + stmtProject := "UPDATE projects SET lifecycle_status = ?, current_workflow_instance_id = NULL WHERE id = ?" + // 写审计 + auditStmt := "INSERT INTO workflow_events ..." + } +} +``` + +**测试用例**: +```go +// 新增测试 +func TestAdvanceStage_CompletesProjectAfterLastStage(t *testing.T) { + // 1. 创建并激活工作流 + // 2. 推进到最后一个启用阶段 + // 3. 所有文档批准后确认推进 + // 4. 验证: + // - workflow_instance.status = "completed" + // - projects.lifecycle_status = "planning" + // - projects.current_workflow_instance_id = NULL + // - 审计事件已记录 +} + +func TestAdvanceStage_DoesNotChangeProjectOnNonLastStage(t *testing.T) { + // 验证:非最后阶段的推进不改变项目状态 +} +``` + +**验收标准**: +- [ ] 单元测试 PASS +- [ ] 完成最后阶段后项目 `lifecycle_status` 变为 `planning` +- [ ] 前端 /projects/{id}/settings 立即显示"配置下一工作流"按钮 +- [ ] 审计日志记录 `ProjectWorkflowCompleted` 事件 + +**预计工作量**: 2h + +--- + +### Task A2: 推进事务不强制更新文档审批状态 + +**目标**: 验证并删除推进时强制批准文档的逻辑 + +**步骤**: +1. 搜索 `AdvanceStage` 中是否存在 UPDATE 文档状态的 SQL +2. 若存在,删除该逻辑 +3. 补充负例测试 + +**搜索命令**: +```bash +grep -n "UPDATE.*doc.*approved\|UPDATE.*approval.*approved" \ + internal/projectinstance/stage_store.go +``` + +**如果存在修改**: +```go +// 🔴 删除以下逻辑(如果存在) +stmt := `UPDATE project_doc_task_instances + SET status = 'approved' + WHERE instance_id = ? AND enabled = true` + +// ✅ 替换为:什么都不改,只改阶段状态 +stmt := `UPDATE project_workflow_instances + SET active_stage_key = ? WHERE id = ?` +``` + +**测试**: +```go +func TestAdvanceStage_DoesNotModifyDocumentApprovalStatus(t *testing.T) { + // 1. 创建、激活、推进到阶段就绪 + // 2. 记录所有文档的当前 approval_status + // 3. 执行 AdvanceStage + // 4. 验证:所有文档 approval_status 未变 +} +``` + +**验收标准**: +- [ ] 单元测试 PASS +- [ ] 推进前后文档状态一致 +- [ ] 阶段状态改变,文档状态不改变 + +**预计工作量**: 1h + +--- + +### Task A3: 禁用文档状态标记为 skipped_disabled + +**目标**: 禁用文档在数据库中有明确的 `skipped_disabled` 状态记录 + +**数据库迁移** (`00025_2026-09-12-disabled-doc-status.sql`): +```sql +-- 新增迁移 +ALTER TABLE project_doc_task_instances +ADD COLUMN status_reason VARCHAR(50) DEFAULT NULL; + +-- 补充约束:disabled 时 duties 必须为空 +ALTER TABLE project_doc_task_instances +ADD CONSTRAINT chk_disabled_no_duties +CHECK (status != 'skipped_disabled' OR + (author_user_id IS NULL AND + reviewer_user_id IS NULL AND + approver_user_id IS NULL)); +``` + +**代码修改** (`internal/projectinstance/store.go`): +```go +func (s *pgStore) SaveWorkflowPlan(...) error { + // 激活时为所有文档创建记录 + for _, stage := range plan.Stages { + for _, doc := range stage.DocTasks { + if doc.Enabled { + // 写启用文档 + stmt := `INSERT INTO project_doc_task_instances + (instance_id, task_key, status, author_user_id, ...) + VALUES (?, ?, 'awaiting_edit', ?, ...)` + } else { + // ✅ 新增:写禁用文档 + stmt := `INSERT INTO project_doc_task_instances + (instance_id, task_key, status, status_reason) + VALUES (?, ?, 'skipped_disabled', 'creator_disabled')` + } + } + } +} +``` + +**测试**: +```go +func TestDisabledDocumentPersistence(t *testing.T) { + // 1. 创建计划,禁用某文档 + // 2. 激活 + // 3. 验证: + // - 禁用文档在 project_doc_task_instances 中存在 + // - status = 'skipped_disabled' + // - 无职责行 +} + +func TestDisabledDocumentRejectionAllEntries(t *testing.T) { + // 针对不同入口测试禁用文档拒绝 + // - API 启动:409 DOC_TASK_DISABLED + // - 提交/审核/批准:403 DOC_DUTY_FORBIDDEN +} +``` + +**验收标准**: +- [ ] 数据库迁移成功应用 +- [ ] 禁用文档在 project_doc_task_instances 中有记录 +- [ ] 禁用文档的所有操作返回 409 或 403 +- [ ] 审计查询显示哪些文档被禁用 + +**预计工作量**: 3h + +--- + +### Task A4: 对话推进切换到新实例服务 + +**目标**: 对话中的"推进阶段"调用新的实例服务,而非旧的商务执行器 + +**修改文件**: `internal/coordinator/engine.go:672-811` + +**原代码**: +```go +func (e *CoordinatorEngine) handleAdvanceStage(...) { + // ...识别成功后 + + // 🔴 旧逻辑:调用商务服务 + result, err := e.bizdevSvc.ExecuteStage(ctx, projectID, ...) +} +``` + +**新代码**: +```go +func (e *CoordinatorEngine) handleAdvanceStage(...) { + // ...识别成功后 + + // ✅ 新逻辑:调用实例服务 + + // 1. 从项目或上文获取 workflow_instance_id + instanceID, err := extractWorkflowInstanceID(ctx, projectID, ...) + if err != nil { + return rejectResponse("Cannot determine active workflow instance") + } + + // 2. 调用新服务 + result, err := e.instSvc.AdvanceStage( + ctx, instanceID, userID, isCreator, + projectinstance.AdvanceStageInput{ + StageKey: stageKey, + ExpectedConfigRevision: configRev, + }, + ) + if err != nil { + return rejectResponse(err.Error()) + } + + // 3. 返回成功响应 + return confirmResponse("Stage advanced successfully", result) +} +``` + +**辅助函数**: +```go +func (e *CoordinatorEngine) extractWorkflowInstanceID( + ctx context.Context, + projectID int64, + dialogueContext map[string]any, +) (int64, error) { + // 优先级: + // 1. 上下文中显式提供 + if id, ok := dialogueContext["workflow_instance_id"].(int64); ok && id > 0 { + return id, nil + } + + // 2. 从项目设置查询 live instance + settings, err := e.instSvc.GetSettings(ctx, projectID, userID, true) + if err != nil || settings.LiveInstance == nil { + return 0, fmt.Errorf("no active workflow instance") + } + return settings.LiveInstance.ID, nil +} +``` + +**测试**: +```go +func TestDialogueAdvanceStage_UsesNewService(t *testing.T) { + // 1. 激活工作流实例 + // 2. 对话说"推进到下一阶段" + // 3. 验证: + // - 调用 instSvc.AdvanceStage 而非 bizdevSvc.ExecuteStage + // - 返回新格式响应 + // - 实例状态正确更新 +} +``` + +**验收标准**: +- [ ] 对话推进测试 PASS +- [ ] 新实例服务被调用(可通过 mock 验证) +- [ ] 权限检查一致(仅创建者可推进) +- [ ] 旧商务服务逻辑不再触发 + +**预计工作量**: 2h + +--- + +### Task A5: 禁用文档拒绝逻辑一致性(DocumentAccessDecider) + +**目标**: 统一所有入口对禁用文档的拒绝决策 + +**新增文件**: `internal/projectinstance/document_access.go` + +```go +package projectinstance + +import "context" + +type DocumentAccessDecider interface { + // CanAccess 判断用户是否可访问指定文档任务 + // action: "edit", "submit", "review", "approve", "view" + CanAccess( + ctx context.Context, + docTaskInstID int64, + action string, + userID int64, + ) (bool, error) + + // GetAccessDenialReason 返回具体的拒绝原因 + GetAccessDenialReason( + ctx context.Context, + docTaskInstID int64, + userID int64, + ) (code string, message string) +} + +type defaultAccessDecider struct { + store Store + docs DocumentReadinessChecker +} + +func (d *defaultAccessDecider) CanAccess( + ctx context.Context, + docTaskInstID int64, + action string, + userID int64, +) (bool, error) { + // 1. 查询 doc_task_instance + docTask, err := d.store.GetDocTaskInstance(ctx, docTaskInstID) + if err != nil { + return false, err + } + + // 2. 检查状态 + if docTask.Status == "skipped_disabled" { + return false, nil // 禁用文档,拒绝所有操作 + } + + // 3. 获取实例详情 + instance, err := d.store.GetInstanceByID(ctx, docTask.InstanceID) + if err != nil { + return false, err + } + + // 4. 检查实例状态 + if instance.Instance.Status != InstanceActive && + instance.Instance.Status != InstanceAwaiting { + return false, nil // 实例非运行状态 + } + + // 5. 检查当前阶段 + if !stageContainsTask(instance, docTask.StageKey, docTaskInstID) { + return false, nil // 不在当前阶段 + } + + // 6. 检查职责匹配 + if !userHasRole(docTask, userID, action) { + return false, nil // 用户非对应职责人 + } + + return true, nil +} +``` + +**集成点**: +- API 路由: `POST /api/v1/doc-task-instances/{id}/runs` +- SSE 推送: 过滤禁用/非当前阶段的文档更新 +- 异步任务: 启动前校验 +- Agent tool: 文档访问 wrapper + +**测试**: +```go +func TestDocumentAccessDecider_DisabledDocumentRejected(t *testing.T) { + // 禁用文档的所有操作被拒绝 +} + +func TestDocumentAccessDecider_NonCurrentStageRejected(t *testing.T) { + // 不在当前阶段的文档被拒绝 +} + +func TestDocumentAccessDecider_NonResponsibleUserRejected(t *testing.T) { + // 非职责人的操作被拒绝 +} + +func TestDocumentAccessDecider_ValidAccessAllowed(t *testing.T) { + // 满足所有条件时允许 +} +``` + +**验收标准**: +- [ ] 所有入口(API、SSE、异步任务、Agent tool)使用同一决策器 +- [ ] 禁用文档全拒绝 +- [ ] 非职责人拒绝 +- [ ] 非当前阶段拒绝 + +**预计工作量**: 4h + +--- + +### Task A6: 激活 Token 安全处理 + +**目标**: Token 改为 one-time、hash 存储、原子消费 + +**修改流程**: + +**步骤 1**: 修改激活预览 API +```go +// 旧:返回 activation_token +GET /api/v1/projects/{id}/activation-preview +→ { token: "xyz123", ... } + +// ✅ 新:返回 challenge 和 nonce +GET /api/v1/projects/{id}/activation-preview +→ { challenge_id: "ch_abc", nonce: "n123", ... } +``` + +**步骤 2**: 修改激活 API +```go +// 旧:明文 token +POST /api/v1/projects/{id}/workflow-instances/activate +{ token: "xyz123" } + +// ✅ 新:挑战响应 +POST /api/v1/projects/{id}/workflow-instances/activate +{ + challenge_id: "ch_abc", + token: "at_oneTimeXyz123" // 由服务端生成 +} +``` + +**步骤 3**: 数据库变更 +```sql +-- 激活挑战表 +CREATE TABLE activation_challenges ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL, + created_by BIGINT NOT NULL, + challenge_id VARCHAR(32) UNIQUE, + nonce VARCHAR(32), + config_hash VARCHAR(64), -- 冻结内容的 hash + created_at TIMESTAMP DEFAULT NOW(), + expires_at TIMESTAMP, + consumed_at TIMESTAMP, + FOREIGN KEY (project_id) REFERENCES projects(id) +); + +-- 一次性 token 表 +CREATE TABLE one_time_tokens ( + id BIGSERIAL PRIMARY KEY, + challenge_id VARCHAR(32), + token_hash VARCHAR(255), -- bcrypt hash + created_at TIMESTAMP, + expires_at TIMESTAMP, + consumed_at TIMESTAMP +); +``` + +**激活逻辑**: +```go +func (s *Service) ActivateWorkflow( + ctx context.Context, + projectID int64, + userID int64, + challengeID string, + token string, +) (*ActivateResult, error) { + return s.store.ActivateInTx(ctx, func(tx Tx) error { + // 1. 查询 challenge(5 分钟未过期) + challenge, err := tx.GetChallenge(ctx, challengeID) + if err != nil || challenge.ExpiresAt < now { + return ErrChallengeExpired + } + + // 2. 验证 token hash + if err := bcrypt.CompareHashAndPassword(challenge.TokenHash, token); err != nil { + return ErrInvalidToken + } + + // 3. 检查 token 未消费 + if challenge.ConsumedAt != nil { + return ErrTokenAlreadyConsumed + } + + // 4. 在同一事务内激活实例 + instance, err := tx.ActivateInstance(ctx, projectID, ...) + if err != nil { + return err + } + + // 5. 标记 challenge 和 token 已消费 + tx.MarkChallengeConsumed(ctx, challengeID) + + return nil + }) +} +``` + +**测试**: +```go +func TestActivationTokenSecurity_OneTimeUse(t *testing.T) { + // 同一 token 不能消费两次 +} + +func TestActivationTokenSecurity_Expiration(t *testing.T) { + // 5 分钟后 token 过期 +} + +func TestActivationTokenSecurity_InvalidTokenRejected(t *testing.T) { + // 错误 token 被拒绝 +} + +func TestActivationTokenSecurity_AtomicConsumption(t *testing.T) { + // token 消费与实例激活在同一事务内 +} +``` + +**验收标准**: +- [ ] Token 为 one-time,消费后不可重用 +- [ ] Token 5 分钟过期 +- [ ] Token hash 存储,明文不保存 +- [ ] 激活原子性(token 消费与实例激活同事务) +- [ ] 中间故障时 token 仍有效(可重试) + +**预计工作量**: 3h + +--- + +## 第二部分:高优先级新功能(Phase B) + +### Task B1: 受控终止工作流 API + +**目标**: 工作流实例可被创建者受控终止,写入原因并审计 + +**新增文件**: `internal/projectinstance/terminate.go` + +```go +package projectinstance + +import "context" + +type TerminationPreviewInput struct { + InstanceID int64 `json:"instance_id"` +} + +type TerminationPreviewResult struct { + InstanceID int64 `json:"instance_id"` + PendingDocuments []DocTaskSummary `json:"pending_documents"` + PendingNotifications []string `json:"pending_notifications"` + InProgressRunTasks []string `json:"in_progress_run_tasks"` + CanTerminate bool `json:"can_terminate"` + Impact string `json:"impact,omitempty"` +} + +type TerminateInput struct { + InstanceID int64 `json:"instance_id"` + Reason string `json:"reason"` // 必填,最多 500 字 + Confirm bool `json:"confirm"` // 二次确认 +} + +type TerminateResult struct { + InstanceID int64 `json:"instance_id"` + Status string `json:"status"` // "terminated" + TerminatedAt time.Time `json:"terminated_at"` + TerminatedBy int64 `json:"terminated_by"` + Reason string `json:"reason"` +} + +func (s *Service) GetTerminationPreview( + ctx context.Context, + instanceID int64, +) (*TerminationPreviewResult, error) { + instance, err := s.store.GetInstanceByID(ctx, instanceID) + if err != nil { + return nil, err + } + + // 收集影响项 + pending := []DocTaskSummary{} + notifs := []string{} + tasks := []string{} + + // ...查询待审批文档、待发送通知、在途任务... + + return &TerminationPreviewResult{ + InstanceID: instanceID, + PendingDocuments: pending, + PendingNotifications: notifs, + InProgressRunTasks: tasks, + CanTerminate: true, + Impact: fmt.Sprintf( + "%d pending docs, %d notifications, %d task runs will be cancelled", + len(pending), len(notifs), len(tasks), + ), + }, nil +} + +func (s *Service) TerminateWorkflow( + ctx context.Context, + instanceID int64, + userID int64, + isCreator bool, + input TerminateInput, +) (*TerminateResult, error) { + if !isCreator { + return nil, ErrForbiddenNotCreator + } + if !input.Confirm { + return nil, ErrTerminationNotConfirmed + } + if len(strings.TrimSpace(input.Reason)) == 0 { + return nil, ErrTerminationReasonRequired + } + + result, err := s.store.TerminateInTx(ctx, func(tx Tx) error { + // 1. 获取实例 + instance, err := tx.GetInstanceByID(ctx, instanceID) + if err != nil { + return err + } + + // 2. 检查状态可终止 + if instance.Instance.Status != InstanceActive && + instance.Instance.Status != InstanceAwaiting { + return ErrCannotTerminateInstance + } + + // 3. 标记实例为 terminated + if err := tx.UpdateInstance(ctx, instanceID, map[string]any{ + "status": "terminated", + "terminated_at": now, + "terminated_by": userID, + "termination_reason": input.Reason, + }); err != nil { + return err + } + + // 4. 取消在途任务 + if err := tx.CancelPendingTaskRuns(ctx, instanceID); err != nil { + return err + } + + // 5. 项目改为 planning + if err := tx.UpdateProject(ctx, instance.Instance.ProjectID, map[string]any{ + "lifecycle_status": "planning", + "current_workflow_instance_id": nil, + }); err != nil { + return err + } + + // 6. 写审计 + if err := tx.LogEvent(ctx, "WorkflowTerminated", map[string]any{ + "instance_id": instanceID, + "reason": input.Reason, + "by": userID, + }); err != nil { + return err + } + + return nil + }) + + return &TerminateResult{ + InstanceID: instanceID, + Status: "terminated", + TerminatedAt: now, + TerminatedBy: userID, + Reason: input.Reason, + }, err +} +``` + +**API 路由**: +```go +// server.go +mux.HandleFunc("GET /api/v1/workflow-instances/{id}/termination-preview", + s.handleTerminationPreview) +mux.HandleFunc("POST /api/v1/workflow-instances/{id}/terminate", + s.handleTerminate) +``` + +**测试**: +```go +func TestTerminationPreview_ShowsImpact(t *testing.T) { + // 显示待审批文档、通知、任务清单 +} + +func TestTerminate_RequiresCreator(t *testing.T) { + // 非创建者被拒绝 +} + +func TestTerminate_RequiresConfirmation(t *testing.T) { + // confirm = false 时被拒绝 +} + +func TestTerminate_UpdatesProjectLifecycle(t *testing.T) { + // 终止后项目可配置下一工作流 +} + +func TestTerminate_CancelsPendingTasks(t *testing.T) { + // 在途任务被取消 +} +``` + +**验收标准**: +- [ ] 预览 API 显示完整影响 +- [ ] 仅创建者可终止 +- [ ] 需二次确认和原因 +- [ ] 终止后项目立即可选择下一流程 +- [ ] 审计记录终止原因 + +**预计工作量**: 6h + +--- + +### Task B2: 真实 E2E 测试(PostgreSQL + PI Agent) + +**目标**: 使用真实数据库和 PI Agent 验证三个工作流完整链路 + +**新增文件**: `internal/server/e2e_workflow_integration_test.go` + +**框架**: +```go +package server + +import ( + "context" + "testing" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func setupE2ETestDB(t *testing.T) *pgxpool.Pool { + dbURL := os.Getenv("TEST_DATABASE_URL") + if dbURL == "" { + t.Skip("TEST_DATABASE_URL not set, skipping E2E tests") + } + + pool, err := pgxpool.New(context.Background(), dbURL) + if err != nil { + t.Fatalf("Failed to connect to E2E test DB: %v", err) + } + + // 创建隔离 schema + schema := "e2e_test_" + randomID() + _, err = pool.Exec(context.Background(), "CREATE SCHEMA "+schema) + if err != nil { + t.Fatalf("Failed to create schema: %v", err) + } + + // 运行所有迁移 + if err := runMigrations(t, pool, schema); err != nil { + t.Fatalf("Failed to run migrations: %v", err) + } + + t.Cleanup(func() { + pool.Exec(context.Background(), "DROP SCHEMA "+schema+" CASCADE") + pool.Close() + }) + + return pool +} + +func TestE2E_BizdevWorkflow_Complete(t *testing.T) { + pool := setupE2ETestDB(t) + ctx := context.Background() + + // 1. 创建项目 + projectID := createProject(t, ctx, pool, "Project-Bizdev", "weichen") + + // 2. 获取三工作流目录 + workflows := listWorkflows(t, ctx, pool) + if len(workflows) != 3 { + t.Fatalf("Expected 3 workflows, got %d", len(workflows)) + } + + // 3. 选择商务对接流程 + bizdevWF := findWorkflow(workflows, "main") + plan := createPlan(t, ctx, pool, projectID, bizdevWF.Key) + + // 4. 激活 + instance := activateWorkflow(t, ctx, pool, projectID, plan) + if instance.Status != "active" { + t.Fatalf("Expected instance.status=active, got %s", instance.Status) + } + + // 5. 逐个批准 7 个文档 + docKeys := []string{ + "intake", "research", "feasibility_dev", "feasibility_reg", + "user_requirements", "prelim_design", "cdo_quote", + } + for i, docKey := range docKeys { + approveDocument(t, ctx, pool, instance.ID, docKey) + + // 检查门禁 + readiness := getStageReadiness(t, ctx, pool, instance.ID, "stage_v01") + if i < len(docKeys)-1 { + if readiness.Ready { + t.Fatalf("Stage should not be ready with %d/%d docs approved", + i+1, len(docKeys)) + } + } else { + if !readiness.Ready { + t.Fatalf("Stage should be ready after all docs approved") + } + } + } + + // 6. 推进各阶段 + advanceStage(t, ctx, pool, instance.ID, "stage_v01", 0) + advanceStage(t, ctx, pool, instance.ID, "stage_v05", 0) + + // 7. 完成最后阶段 + result := advanceStage(t, ctx, pool, instance.ID, "stage_v10", 0) + if result.CompletedAt == nil { + t.Fatal("Expected instance.completed_at to be set") + } + + // 8. 验证项目已返回 planning + settings := getProjectSettings(t, ctx, pool, projectID) + if settings.LifecycleStatus != "planning" { + t.Fatalf("Expected project lifecycle=planning, got %s", + settings.LifecycleStatus) + } + + // 9. 可配置下一个工作流 + workflows2 := listWorkflows(t, ctx, pool) + if len(workflows2) != 3 { + t.Fatalf("Cannot select next workflow") + } + + t.Logf("✅ Bizdev workflow complete chain PASSED") +} + +func TestE2E_ProductManagerWorkflow_Standalone(t *testing.T) { + pool := setupE2ETestDB(t) + ctx := context.Background() + + projectID := createProject(t, ctx, pool, "Project-PM", "weichen") + + // 选择产品经理流程(未运行商务流程) + pmWF := findWorkflow(listWorkflows(t, ctx, pool), "product_manager") + plan := createPlan(t, ctx, pool, projectID, pmWF.Key) + instance := activateWorkflow(t, ctx, pool, projectID, plan) + + // 批准 SRS 文档 + approveDocument(t, ctx, pool, instance.ID, "system_reqs") + + // 推进 + advanceStage(t, ctx, pool, instance.ID, "stage_pm", 0) + + // 完成 + result := advanceStage(t, ctx, pool, instance.ID, "stage_pm", 0) + if result.CompletedAt == nil { + t.Fatal("PM workflow should complete") + } + + t.Logf("✅ ProductManager workflow standalone PASSED") +} + +func TestE2E_TestEngineerWorkflow_WithoutSRS(t *testing.T) { + pool := setupE2ETestDB(t) + ctx := context.Background() + + projectID := createProject(t, ctx, pool, "Project-TE", "weichen") + + // 直接选择测试工程师流程,不需要先运行产品经理 + teWF := findWorkflow(listWorkflows(t, ctx, pool), "test_manager") + plan := createPlan(t, ctx, pool, projectID, teWF.Key) + instance := activateWorkflow(t, ctx, pool, projectID, plan) + + // PI Agent 生成测试方案(集成或 mock) + // ... + + // 批准测试方案 + approveDocument(t, ctx, pool, instance.ID, "test_manager") + + // 推进完成 + result := advanceStage(t, ctx, pool, instance.ID, "stage_te", 0) + if result.CompletedAt == nil { + t.Fatal("TE workflow should complete") + } + + t.Logf("✅ TestEngineer workflow without SRS PASSED") +} +``` + +**验收标准**: +- [ ] 商务流程 7 文档全流程完成 +- [ ] 产品流程独立运行无前置依赖 +- [ ] 测试流程无 SRS 也能运行 +- [ ] 每个完成流程后项目可配置下一个 +- [ ] 所有 E2E 测试 PASS +- [ ] 真实 PG + PI Agent 完整链路验证 + +**预计工作量**: 8h + +--- + +### Task B3: 项目上下文快照服务(ProjectContextSnapshot) + +**目标**: 为产品/测试流程提供统一、可复现的项目上下文 + +**新增文件**: `internal/projectcontext/service.go` + +**核心结构**: +```go +package projectcontext + +import "context" + +type ContextSnapshot struct { + ID int64 `json:"id"` + ProjectID int64 `json:"project_id"` + TaskID int64 `json:"task_id"` // 引用的任务(可选) + SnapshotHash string `json:"snapshot_hash"` + CreatedAt time.Time `json:"created_at"` + CreatedBy int64 `json:"created_by"` + + // 五层内容 + ProjectFacts ProjectFacts `json:"project_facts"` + ProjectDecisions ProjectDecisions `json:"project_decisions"` + ProjectDocuments ProjectDocuments `json:"project_documents"` + ProjectDialogue []DialogueEntry `json:"project_dialogue"` + HistoryOutputs []HistoryOutput `json:"history_outputs"` + KnowledgeHits []KnowledgeRef `json:"knowledge_hits"` + + // 血缘 + Sources []SourceManifest `json:"sources"` + ExcludedPrivate int `json:"excluded_private_count"` + ExcludedNoAccess int `json:"excluded_no_access_count"` +} + +type SourceManifest struct { + SourceType string // "project_fact", "document", "dialogue", etc. + SourceID int64 + SourceVersion int + SourceHash string + AuthorityLevel string // "confirmed", "approved", "reference", "evidence" + Permission string // "all_members", "shared_to_project", "private" + WasReferenced bool // 是否实际被输出引用 +} + +// Service 构建和查询项目上下文快照 +type Service struct { + store Store + projectStore projectworkflow.Store + docStore DocumentStore + dialogueStore DialogueStore +} + +func (s *Service) BuildContextSnapshot( + ctx context.Context, + projectID int64, + taskID int64, // 可选,用于任务的输入 +) (*ContextSnapshot, error) { + // 1. 收集五层源 + facts, err := s.gatherProjectFacts(ctx, projectID) + if err != nil { + return nil, err + } + + decisions, err := s.gatherProjectDecisions(ctx, projectID) + if err != nil { + return nil, err + } + + docs, sources, err := s.gatherProjectDocuments(ctx, projectID) + if err != nil { + return nil, err + } + + dialogue, excluded, err := s.gatherProjectDialogue(ctx, projectID) + if err != nil { + return nil, err + } + + history, err := s.gatherHistoryOutputs(ctx, projectID) + if err != nil { + return nil, err + } + + knowledge, err := s.gatherKnowledgeHits(ctx, projectID) + if err != nil { + return nil, err + } + + // 2. 冲突合并 + merged := s.mergeConflicts(facts, decisions, docs, dialogue, history) + + // 3. 计算 hash + hash := computeContentHash(merged) + + // 4. 保存快照 + snapshot := &ContextSnapshot{ + ProjectID: projectID, + TaskID: taskID, + SnapshotHash: hash, + CreatedAt: now, + CreatedBy: userID, + ProjectFacts: facts, + ProjectDecisions: decisions, + ProjectDocuments: docs, + ProjectDialogue: dialogue, + HistoryOutputs: history, + KnowledgeHits: knowledge, + Sources: sources, + ExcludedPrivate: excluded.PrivateCount, + ExcludedNoAccess: excluded.NoAccessCount, + } + + if err := s.store.SaveSnapshot(ctx, snapshot); err != nil { + return nil, err + } + + return snapshot, nil +} + +func (s *Service) gatherProjectFacts( + ctx context.Context, + projectID int64, +) (ProjectFacts, error) { + // 查询 projects 表和结构化事实黑板 + project, err := s.projectStore.GetProject(ctx, projectID) + if err != nil { + return ProjectFacts{}, err + } + + return ProjectFacts{ + ProjectName: project.Name, + CustomerName: project.Customer, + ProductType: project.ProductType, + IntendedUse: project.IntendedUse, + TargetMarket: project.TargetMarket, + StructuredFacts: project.StructuredFacts, + AuthorityLevel: "confirmed", // 项目元数据最权威 + }, nil +} + +func (s *Service) gatherProjectDocuments( + ctx context.Context, + projectID int64, +) (ProjectDocuments, []SourceManifest, error) { + // 查询当前有效版本的受控文档、共享文档、上传材料 + docs, err := s.docStore.ListProjectDocuments(ctx, projectID) + if err != nil { + return ProjectDocuments{}, nil, err + } + + sources := make([]SourceManifest, 0) + for _, doc := range docs { + sources = append(sources, SourceManifest{ + SourceType: "document", + SourceID: doc.ID, + SourceVersion: doc.LatestApprovedVersion, + SourceHash: doc.ContentHash, + AuthorityLevel: "approved", // 已批准文档 + Permission: "all_members", + WasReferenced: false, // 待填充 + }) + } + + return ProjectDocuments{ + ControlledDocs: docs, + AuthorityLevel: "approved", + }, sources, nil +} + +func (s *Service) gatherProjectDialogue( + ctx context.Context, + projectID int64, +) ([]DialogueEntry, ExcludedCounts, error) { + // 查询所有进入项目知识边界的对话 + // 过滤:私人草稿、个人私聊(未共享)不纳入 + + dialogue, excluded, err := s.dialogueStore.ListProjectDialogue( + ctx, projectID, true, // 仅已共享 + ) + + return dialogue, excluded, err +} + +// 冲突优先级: +// 确认事实/决策 > 已批准文档 > 上传材料 > 里程碑 > 普通对话 > 总结 +func (s *Service) mergeConflicts( + facts ProjectFacts, + decisions ProjectDecisions, + docs ProjectDocuments, + dialogue []DialogueEntry, + history []HistoryOutput, +) MergedContext { + // 若多个来源对同一概念有不同看法,按优先级择一 + // 例:需求既在 SRS 中又在对话中,选 SRS + + return MergedContext{ + // ... + } +} +``` + +**与现有系统集成**: +- 产品经理:调用 `ctxSvc.BuildContextSnapshot` 取数代替现有部分逻辑 +- 测试工程师:同样使用新快照,而非硬查 SRS +- 血缘追溯:输出文档内包含 source manifest 和追溯表 + +**测试**: +```go +func TestProjectContextSnapshot_ConsistentForSameInput(t *testing.T) { + // 同一项目在同一时刻的快照 hash 相同 +} + +func TestProjectContextSnapshot_ChangeWhenSourceUpdated(t *testing.T) { + // 新批准文档后快照 hash 改变 +} + +func TestProjectContextSnapshot_ExcludesPrivateContent(t *testing.T) { + // 私人草稿和未共享对话不纳入 +} + +func TestProjectContextSnapshot_LineageComplete(t *testing.T) { + // 输出包含所有被引用来源的 manifest +} +``` + +**验收标准**: +- [ ] 快照服务实现完整 +- [ ] 产品/测试工程师都使用新服务 +- [ ] 快照可复现(同输入 → 同快照) +- [ ] 血缘追溯表完整 + +**预计工作量**: 12h + +--- + +## 第三部分:验收与发布(Phase C) + +### Task C1-C7: 见前面 Phase C 表 + +**总体验收检查表**: + +``` +□ 单元测试覆盖 + □ 所有 bug fix 有负例测试 + □ 新功能覆盖率 > 50% + □ 关键路径覆盖率 = 100% + +□ 集成测试 + □ PG 集成测试全 PASS + □ 并发测试 PASS + □ 数据库迁移向前向后兼容 + +□ 真实 E2E(PostgreSQL + PI Agent) + □ 商务流程 PASS + □ 产品流程 PASS + □ 测试流程 PASS + □ 项目完成后切换 PASS + □ 受控终止 PASS + +□ 验收标准(见 7.1-7.5) + □ 目录仅三项 + □ 禁用文档全拒绝 + □ 推进不改文档状态 + □ 完成后可配置下一流程 + □ 安全与审计完整 + +□ 文档与发布 + □ API 文档更新 + □ 发布说明完成 + □ 迁移脚本与回滚 + □ 用户指南 + +□ 生产就绪检查 + □ 性能基准测试 + □ 压力测试 + □ 故障场景演练 + □ 审计日志覆盖 +``` + +--- + +**完整拆分总结**: +- **Phase A** (Day 1): 15 小时 - 关键缺陷修复 +- **Phase B** (Day 2-3): 26 小时 - 新功能开发 +- **Phase C** (Day 4-5): 20 小时 - 完善与发布 + +**总计**: ~61 小时 + +--- + +**下一步**: 选择一项任务开始实施,或全队并行进行。