From a39823da388fb880f44a2b16e13048560457e97f Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 17 Sep 2026 07:16:06 +0000 Subject: [PATCH] fix(docgov): pass dynamic StageKey into document ledger create Stop hardcoding V0.5_feasibility when creating docgov records; prefer frozen-instance StageKey from TaskRunRequest with main-task fallback. Co-authored-by: Cursor --- .../2026-09-17-rdev-workflow-integration.md | 64 +++++++++++++++++++ internal/bizdev/resolve_doc_stage_test.go | 30 +++++++++ internal/bizdev/service.go | 23 ++++++- internal/bizdev/types.go | 1 + internal/server/handlers.go | 1 + internal/server/task_run_handlers.go | 5 ++ .../server/task_run_request_build_test.go | 18 ++++++ 7 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 .doc/ops/short-term/2026-09-17-rdev-workflow-integration.md create mode 100644 internal/bizdev/resolve_doc_stage_test.go diff --git a/.doc/ops/short-term/2026-09-17-rdev-workflow-integration.md b/.doc/ops/short-term/2026-09-17-rdev-workflow-integration.md new file mode 100644 index 00000000..c14c9c59 --- /dev/null +++ b/.doc/ops/short-term/2026-09-17-rdev-workflow-integration.md @@ -0,0 +1,64 @@ +--- +title: "研发工作流(rdev)接入:13 阶段 / 60 文档任务" +status: in_progress +created: 2026-09-17 +updated: 2026-09-17 +owner: Cursor +branch: fix/docgov-stage-dynamic → feature/rdev-core +worktree: "-" +mutex: workflow-def +--- + +# 研发工作流(rdev)接入:13 阶段 / 60 文档任务 + +## 目标 + +在现有 5 个正式工作流外新增医疗第 4 工作流「研发设计控制流程」(key: `rdev`),完整承载参考项目 13 phase / 60 文档任务的 ISO 13485 DHF 设计控制链,并通过真实 PI 验收;随后完成全工作流任务键命名空间化(P4)。 + +## 范围 + +- 方案:[07-研发工作流rdev接入-设计与实施计划](../../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md)(P0–P4 五期;§8 四项决策已确认) +- P0:docgov Stage 去硬编码 + stage key 透传 +- P1:rdev.v2.json manifest(13×60 一次到位)+ 60 份 rd_ 前缀模板 + catalog/seed/registry + squad kind 编排 + main-completed precheck + 齐套测试 + stub PG 全链路 +- P2:前端激活规划/切换/前置 Banner + intent 文档名匹配 + prior docs 注入与血缘 +- P3:真实 PI Agent E2E(rd_proposal / rd_planning_review / rd_sw_plan 三样本)+ CI 门禁扩四 +- P4:现有工作流(main/pm/tm + 软件两盘点)任务键 `bd_`/`pm_`/`tm_`(`sw_`)空间化,alias 兼容式改名(D12),独立分支独立验收 + +## 非目标 + +- 二期增强全部移入 backlog:[2026-09-17-rdev-phase2-backlog](../../backlog/2026-09-17-rdev-phase2-backlog.md)(N/A 不适用页生成、UR/RS 结构化门禁、kind 专用门禁、depends_on 精确血缘、DMR 归档、sw_ 键盘点、complexity regulatory 注入) +- 云盘 canonical DHF/DMR 归档(本仓库无云盘模块) +- 软件域(non_medical_software)开放 rdev + +## 步骤 + +1. [x] 用户确认方案 §8 四项决策(2026-09-17:名称「研发设计控制流程」/ 60 任务一次到位 / N/A 规划期关闭 + backlog / rd_ 前缀并扩展到现有工作流键名空间化) +2. [ ] P0 分支 `fix/docgov-stage-dynamic` + PG 回归 +3. [ ] P1 分支 `feature/rdev-core`(manifest/模板/seed/编排/precheck/齐套测试) +4. [ ] P2 分支 `feature/rdev-web-governance`(前端 + intent + 血缘)— 骨架已定,本期不实现 +5. [ ] P3 真实 PI 验收 + 报告归档 + FEATURE-MATRIX 更新 — 骨架已定,本期不实现 +6. [ ] P4 分支 `feature/workflow-key-namespace`(D12 alias 兼容式改名 + 双路径回归)— 骨架已定,本期不实现 + +## 验收标准 + +- [ ] `go vet ./...` && `go test -short ./...` PASS +- [ ] PG 集成:rdev 实例 planning→live→13 阶段推进→completed;precheck 拒绝路径 PASS +- [ ] 齐套断言:manifest 60 任务 ↔ `templates/docs/rd_*.md` 存在非空;rd_ 键与既有任务键不相交 +- [ ] web `npm test && npm run build` PASS;Playwright 抽检激活/推进 +- [ ] 真实 PI + MiniMax 三样本 E2E 报告归档 `.doc/test-reports/` +- [ ] P4:旧 live 实例(旧键)+ 新实例(新键)双路径回归 PASS;全工作流任务键集合无交集 + +## 相关链接 + +- 设计方案: [07-研发工作流rdev接入-设计与实施计划](../../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md) +- 参考调研: [2026-09-17-rdev-workflow-reference-analysis](../research/2026-09-17-rdev-workflow-reference-analysis.md) +- 二期 backlog: [2026-09-17-rdev-phase2-backlog](../../backlog/2026-09-17-rdev-phase2-backlog.md) +- Checklist: - +- Handoff: - +- Test report: - + + +## 证据 / 备注 + +- 参考项目:`.refs/weichen-agent-system`(configs/workflows/rdev.yaml 60 stage、templates/docs 60 模板、squads/overlays 配套) +- 关键约束:WF-DEF-06(无流程级依赖,precheck 先例 test_manager);doc_id `doc_proj{pid}_{taskKey}` 平铺命名 → rd_ 前缀 diff --git a/internal/bizdev/resolve_doc_stage_test.go b/internal/bizdev/resolve_doc_stage_test.go new file mode 100644 index 00000000..fa5c1e72 --- /dev/null +++ b/internal/bizdev/resolve_doc_stage_test.go @@ -0,0 +1,30 @@ +package bizdev + +import "testing" + +func TestResolveDocStage_PrefersStageKey(t *testing.T) { + got := resolveDocStage(TaskRunRequest{TaskKey: "intake", StageKey: "RD01_planning"}) + if got != "RD01_planning" { + t.Fatalf("got %q, want RD01_planning", got) + } +} + +func TestResolveDocStage_MainTaskFallback(t *testing.T) { + cases := []struct { + task string + want string + }{ + {"intake", string(StageV01_Triage)}, + {"research", string(StageV05_Feasibility)}, + {"feasibility_dev", string(StageV05_Feasibility)}, + {"cdo_quote", string(StageV10_Quoting)}, + {"system_reqs", string(StageV05_Feasibility)}, + {"", string(StageV05_Feasibility)}, + } + for _, tc := range cases { + got := resolveDocStage(TaskRunRequest{TaskKey: tc.task}) + if got != tc.want { + t.Fatalf("task=%q got %q want %q", tc.task, got, tc.want) + } + } +} diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index 78869523..dc84417e 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -287,6 +287,24 @@ func (s *Service) ListTasks() []TaskMeta { return s.squad.ListTasks() } +// resolveDocStage 解析写入文档账本的 Stage 字段。 +// 优先请求透传的冻结实例阶段键;否则按 main 三阶段任务映射;再回落 V0.5_feasibility。 +func resolveDocStage(req TaskRunRequest) string { + if sk := strings.TrimSpace(req.StageKey); sk != "" { + return sk + } + switch strings.TrimSpace(req.TaskKey) { + case "intake": + return string(StageV01_Triage) + case "research", "feasibility_dev", "feasibility_reg", "user_requirements", "prelim_design": + return string(StageV05_Feasibility) + case "cdo_quote": + return string(StageV10_Quoting) + default: + return string(StageV05_Feasibility) + } +} + func (s *Service) ListStages() []StageMeta { return SupportedStages } @@ -476,17 +494,18 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun var appliedProposalID string if s.docGov != nil && strings.TrimSpace(docID) != "" { + docStage := resolveDocStage(req) var createErr error if s.createDocumentHook != nil { docRec, createErr = s.createDocumentHook( ctx, docID, req.ProjectID, req.TaskKey, artifactTitle, - "V0.5_feasibility", req.WorkDir, + docStage, req.WorkDir, authorID, authorName, reviewerID, reviewerName, approverID, approverName, ) } else { docRec, createErr = s.docGov.CreateOrGetDocument( ctx, docID, req.ProjectID, req.TaskKey, artifactTitle, - "V0.5_feasibility", req.WorkDir, + docStage, req.WorkDir, authorID, authorName, reviewerID, reviewerName, approverID, approverName, ) } diff --git a/internal/bizdev/types.go b/internal/bizdev/types.go index e8f65d14..bb2ea531 100644 --- a/internal/bizdev/types.go +++ b/internal/bizdev/types.go @@ -63,6 +63,7 @@ type TaskRunRequest struct { UserContext string `json:"user_context,omitempty"` MemoryBankFiles map[string]string `json:"memory_bank_files,omitempty"` DocID string `json:"doc_id,omitempty"` + StageKey string `json:"stage_key,omitempty"` // 冻结实例阶段键;空则 resolveDocStage 回落 AuthorID int64 `json:"author_id,omitempty"` AuthorName string `json:"author_name,omitempty"` ReviewerID int64 `json:"reviewer_id,omitempty"` diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 130846a6..bd334210 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -68,6 +68,7 @@ type TaskRunRequestDTO struct { UserID int64 `json:"user_id,omitempty"` ProjectID int64 `json:"project_id,omitempty"` DocID string `json:"doc_id,omitempty"` + StageKey string `json:"stage_key,omitempty"` // 冻结实例阶段键(服务端 ResolveDocTask 注入) AuthorID int64 `json:"author_id,omitempty"` AuthorName string `json:"author_name,omitempty"` ReviewerID int64 `json:"reviewer_id,omitempty"` diff --git a/internal/server/task_run_handlers.go b/internal/server/task_run_handlers.go index 190fbe54..587da2d4 100644 --- a/internal/server/task_run_handlers.go +++ b/internal/server/task_run_handlers.go @@ -201,6 +201,9 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) { } taskReq := s.buildBizdevTaskRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, claimed.RunID) + if taskScope != nil && strings.TrimSpace(taskScope.StageKey) != "" { + taskReq.StageKey = taskScope.StageKey + } resumeDir := req.WorkDir stageReq := s.buildBizdevStageRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, claimed.RunID) createdRun, err := s.taskRuns.StartClaimedRun(r.Context(), claimed, taskReq, stageReq, resumeDir) @@ -592,6 +595,7 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) { UserID: rec.UserID, ProjectID: rec.ProjectID, TaskKey: rec.TaskKey, + StageKey: rec.WorkflowStageKey, }, rec.SessionID, rec.ClientTurnID, rec.RunID) stageReq := s.buildBizdevStageRequest(TaskRunRequestDTO{ UserID: rec.UserID, @@ -653,6 +657,7 @@ func (s *Server) buildBizdevTaskRequest(dto TaskRunRequestDTO, sessionID, client ClientTurnID: clientTurnID, RunID: runID, DocID: dto.DocID, + StageKey: dto.StageKey, AuthorID: dto.AuthorID, AuthorName: dto.AuthorName, ReviewerID: dto.ReviewerID, diff --git a/internal/server/task_run_request_build_test.go b/internal/server/task_run_request_build_test.go index 1caeb2d0..53042e56 100644 --- a/internal/server/task_run_request_build_test.go +++ b/internal/server/task_run_request_build_test.go @@ -45,3 +45,21 @@ func TestBuildBizdevStageRequest_PreservesIdentityAndRoles(t *testing.T) { t.Fatalf("stage=%q", got.Stage) } } + +func TestBuildBizdevTaskRequest_PassesStageKey(t *testing.T) { + s := &Server{} + dto := TaskRunRequestDTO{ + TaskKey: "rd_proposal", + Backend: "stub", + UserID: 1, + ProjectID: 100, + StageKey: "RD01_planning", + } + got := s.buildBizdevTaskRequest(dto, "sess", "turn", "run") + if got.StageKey != "RD01_planning" { + t.Fatalf("StageKey=%q, want RD01_planning", got.StageKey) + } + if got.TaskKey != "rd_proposal" { + t.Fatalf("TaskKey=%q", got.TaskKey) + } +}