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 <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-17 09:46:09 +00:00
co-authored by Cursor
parent adb48c86a5
commit a39823da38
7 changed files with 140 additions and 2 deletions
@@ -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_ 前缀
+30
View File
@@ -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)
}
}
}
+21 -2
View File
@@ -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,
)
}
+1
View File
@@ -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"`
+1
View File
@@ -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"`
+5
View File
@@ -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,
@@ -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)
}
}