feat(workflow): namespace task keys without breaking old instances

Published manifests now use bd_/pm_/tm_/sw_ prefixes so workflows cannot share a doc id. Frozen instances keep the old keys; lookup, templates, and duty checks treat the two names as the same task.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-19 10:50:25 +00:00
co-authored by Cursor
parent 2a0f8b4549
commit dba8139d56
36 changed files with 544 additions and 190 deletions
@@ -49,7 +49,7 @@ updated: 2026-09-19
| FE App.tsx | 部分。登录壳不再静态引用导出引擎、文件预览、管理台、块编辑器和图表。入口包 2.83 MB → 1.00 MB(br 626 KB → 235 KB)。`loginShellImports.test.ts`。工作台主体仍在 `App.tsx`,未按 hook 拆文件 |
| H1-del | 关闭理由:`allowLegacyTestPrincipal` 仍被 `AllowInMemory && ContextDbHost==""` 的 dev/test 内存测试使用。删除会把这批测试变成未登录 401。保留告警,删除另开、先改测试夹具 |
仍开放:rdev P3 三样本真 PI、P4 键名空间化、首次加载 U1/U6/U7、dev 容器重建与治理库 PG。P2 规划界面已折叠长阶段、可批量纳入人工审批,并在选中 rdev 时提示 CDO 前置。U4 登录扇出收口仍有效,浏览器请求次数证据还没有。2026-09-19 在 `fix/audit-remainder-closeout` 跑过 `cd web && npm test && npm run build`:node:test 414 pass / 0 fail,vitest 32 pass;入口 `index-BFf6n8IA.js` 1,005.01 kB,br 235,285 B。CURRENT 文首 schema 52 与三套容器一致,00053 要等重建后才升。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。
仍开放:rdev P3 三样本真 PI、首次加载 U1/U6/U7、dev 容器重建与治理库 PG。P4 键名空间化已在本分支落地:新清单用带前缀的任务键,旧键与旧 doc_id 仍可读,`TestPublishedTaskKeysAreNamespacedAndDisjoint`。意图短语仍输出旧键,执行路径按别名接上。P2 规划界面已折叠长阶段、可批量纳入人工审批,并在选中 rdev 时提示 CDO 前置。U4 登录扇出收口仍有效,浏览器请求次数证据还没有。2026-09-19 在 `fix/audit-remainder-closeout` 跑过 `cd web && npm test && npm run build`:node:test 414 pass / 0 fail,vitest 32 pass;入口 `index-BFf6n8IA.js` 1,005.01 kB,br 235,285 B。CURRENT 文首 schema 52 与三套容器一致,00053 要等重建后才升。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。
红线关闭(不是实现):
+1 -1
View File
@@ -57,7 +57,7 @@
| 对话数据治理(D7) | 留存/删除口径、库·备份·日志控制、敏感字段加密边界 | deferred | deferred_enhancement(违规则升 blocker) | [计划](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md) Scope Boundaries;[收口 §7.1](../remediation/2026-09-16-partial-deferred-completion-plan.md);[D7 ADR 草案](../adr/2026-09-17-d7-chat-data-retention.md);[W7 盘点](../audit/2026-09-17-d7-data-inventory.md);[报告](../test-reports/2026-09-17-w7-d7-inventory-test-report.md) | **after W7**:只读盘点完成(sessions/messages/KB/日志/备份/event)。**未发现**已确认基线违规,未写 gap-matrix。口径仍 `pending-compliance-signoff`;禁止按 365/180/30 物理删除。本行不升 done | `chat` |
| 智能体小队 | 角色-任务-工作区-黑板 | done | non_blocker | [agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6)属 backlog | `squad` |
| 工作流治理 | 三正式工作流、设置页/DutyMode、动作级鉴权、真实 PI 三工作流 E2E、full/none 混合长链 | done | non_blocker | [三工作流整改](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[409/职责](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md);[PI E2E](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2 full/none](../test-reports/2026-09-16-wave2-real-pi-evidence-test-report.md) | full 跨用户审批 + none 免审批不阻塞已签。纯软件自举仍独立 planned | `workflow` |
| 研发设计控制(rdev) | 医疗第 4 工作流:13×60 manifest/模板、`squad.rdev_doc.v1` kind 编排、main-completed 硬 precheck、StageKey 动态化;不含前端/真 PI/键名空间化 | partial | deferred_enhancement | [设计](../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md);[short-term](../archive/ops/2026-09-17-rdev-workflow-integration.md);[模板深度](../archive/ops/2026-09-17-rdev-template-depth.md);[S0+S1 首批报告](../test-reports/2026-09-17-rdev-template-depth-test-report.md);[S1 续批报告](../test-reports/2026-09-17-rdev-template-depth-s1-continued-test-report.md);[写作规范](../system-design/project-workflow/08-rdev模板写作规范.md);[backlog](../backlog/2026-09-17-rdev-phase2-backlog.md) | **P0+P1 done**。**模板深度 S0+S1 首批 + S1 续批 done**:60/60 模板已补强;真 PI 抽样 PASS(含 `rd_planning_review` + `rd_design_vv`,项目 6446)。剩余:S3 overlay 与 R2/R3 已接。P2 名称匹配、阶段折叠、批量审批已接。选中 rdev 时显示与服务端相同的 CDO 前置说明(`workflowPrerequisite.test.ts`)。P3 三样本真 PI、P4 键名空间化仍未做 | `workflow-def` / `rdev-templates` |
| 研发设计控制(rdev) | 医疗第 4 工作流:13×60 manifest/模板、`squad.rdev_doc.v1` kind 编排、main-completed 硬 precheck、StageKey 动态化;不含前端/真 PI/键名空间化 | partial | deferred_enhancement | [设计](../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md);[short-term](../archive/ops/2026-09-17-rdev-workflow-integration.md);[模板深度](../archive/ops/2026-09-17-rdev-template-depth.md);[S0+S1 首批报告](../test-reports/2026-09-17-rdev-template-depth-test-report.md);[S1 续批报告](../test-reports/2026-09-17-rdev-template-depth-s1-continued-test-report.md);[写作规范](../system-design/project-workflow/08-rdev模板写作规范.md);[backlog](../backlog/2026-09-17-rdev-phase2-backlog.md) | **P0+P1 done**。**模板深度 S0+S1 首批 + S1 续批 done**:60/60 模板已补强;真 PI 抽样 PASS(含 `rd_planning_review` + `rd_design_vv`,项目 6446)。剩余:S3 overlay 与 R2/R3 已接。P2 名称匹配、阶段折叠、批量审批已接。选中 rdev 时显示与服务端相同的 CDO 前置说明(`workflowPrerequisite.test.ts`)。P4 键名空间化已接:发布清单用 `bd_`/`pm_`/`tm_`/`sw_`,旧键仍走同一套步骤和模板,旧 doc_id 仍可查(`keyspace_test.go`)。P3 三样本真 PI 仍未做 | `workflow-def` / `rdev-templates` |
| 纯软件项目自举 | 软件项目创建、Bootstrap、Delivery、受控工程交接 | partial | deferred_enhancement(明确不挡首发) | [设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[ADR](../adr/2026-09-16-software-project-bootstrap-keys.md);[L1 报告](../test-reports/2026-09-16-software-project-bootstrap-test-report.md);[W5a](../test-reports/2026-09-17-w5a-software-bootstrap-ui-pi-test-report.md) | **before**:L1 + UI 仍克隆医疗模板。**after W5a**:catalog 驱动创建、软件不挂医疗 key、docs_only 真 PI 建议 delivery 且不自动激活。**SWBOOT-10 仍 stub**(W5b);本行不升 done | `workflow-design` |
| IAM / 鉴权 | 项目级 API 鉴权 + RBAC;会话/阶段授权 | done | 加固 | [v3 full inventory](../audit/authorization-matrix-v3-full.md);[session 中断](../test-reports/2026-09-16-iam-route-inventory-test-report.md);[fail-closed](../test-reports/2026-09-16-wave1-iam-fail-closed-test-report.md);[410 策略](../audit/2026-09-16-bug11-410-retire-decision.md);[R13](../audit/2026-09-16-r13-data-safety-checklist.md) | 全路由 inventory + session 中断负例 + 410 冻结 + R13 基线已签 | `authz` |
| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | done | non_blocker | [docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[正文 UI](../test-reports/2026-09-14-controlled-content-ui-acceptance.md);[canonical hash](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md) | 项目 143 只读;不终止 | `docgov` |
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"strings"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/workflowdef"
)
// projectInstanceDutyAdapter 将活跃工作流实例的逐文档职责投影为 bizdev 窄接口,
@@ -28,7 +29,7 @@ func (a *projectInstanceDutyAdapter) ResolveGovernedDuty(ctx context.Context, pr
}
for _, st := range detail.Instance.ConfigSnapshot.Stages {
for _, dt := range st.DocTasks {
if dt.TaskKey != taskKey || !dt.Enabled {
if !workflowdef.SameTaskKey(dt.TaskKey, taskKey) || !dt.Enabled {
continue
}
mode, effective, isAuthorOnly := projectinstance.ResolveDocTaskDuty(dt)
+22 -5
View File
@@ -5,11 +5,14 @@ import (
"fmt"
"regexp"
"strings"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/workflowdef"
)
var (
legacyDocPPattern = regexp.MustCompile(`^doc-p(\d+)-(.+)$`)
legacyDocProjHyphenPattern = regexp.MustCompile(`^doc-proj(\d+)-(.+)$`)
legacyDocPPattern = regexp.MustCompile(`^doc-p(\d+)-(.+)$`)
legacyDocProjHyphenPattern = regexp.MustCompile(`^doc-proj(\d+)-(.+)$`)
)
// normalizeDocID 将 legacy 本地 ID 映射为服务端权威 doc_id。
@@ -46,7 +49,7 @@ func (s *Service) ResolveAndValidateDocID(ctx context.Context, req TaskRunReques
return docID, nil
}
doc, err := s.docGov.GetDocument(ctx, docID)
doc, err := lookupDocument(ctx, s, docID)
if err != nil {
// 尚未建档:与空 doc_id 行为一致,交由 CreateOrGetDocument 创建
if fallback := canonicalDocID(req.ProjectID, req.TaskKey); fallback != "" {
@@ -61,9 +64,23 @@ func (s *Service) ResolveAndValidateDocID(ctx context.Context, req TaskRunReques
if req.ProjectID > 0 && doc.ProjectID != req.ProjectID {
return "", fmt.Errorf("doc_id %s does not belong to project %d", docID, req.ProjectID)
}
// 已注册文档的 task_key 以账本为准;客户端 doc_id 不得改写或覆盖请求 task_key。
if req.TaskKey != "" && doc.TaskKey != "" && doc.TaskKey != req.TaskKey {
// 已注册文档的 task_key 以账本为准。旧键与新键是同一任务,不算改写。
if req.TaskKey != "" && doc.TaskKey != "" && !workflowdef.SameTaskKey(doc.TaskKey, req.TaskKey) {
return "", fmt.Errorf("doc_id %s task_key mismatch: expected %s got %s", docID, req.TaskKey, doc.TaskKey)
}
return doc.DocID, nil
}
func lookupDocument(ctx context.Context, s *Service, docID string) (*docgov.DocumentRecord, error) {
doc, err := s.docGov.GetDocument(ctx, docID)
if err == nil {
return doc, nil
}
for _, alt := range workflowdef.AlternateDocIDs(docID) {
altDoc, altErr := s.docGov.GetDocument(ctx, alt)
if altErr == nil {
return altDoc, nil
}
}
return nil, err
}
+5 -4
View File
@@ -11,6 +11,7 @@ import (
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/stream"
"chat-agent-system/internal/workflowdef"
)
// executeStageDrainingPending runs ExecuteStage, confirming high-risk agent candidates
@@ -139,7 +140,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) {
if !resV01.Success {
t.Fatalf("Stage V0.1 should succeed, got error: %s", resV01.ErrorReason)
}
if resV01.PrimaryArtifact == nil || resV01.PrimaryArtifact.TaskKey != "intake" {
if resV01.PrimaryArtifact == nil || !workflowdef.SameTaskKey(resV01.PrimaryArtifact.TaskKey, "intake") {
t.Fatalf("Stage V0.1 primary artifact should be intake, got: %+v", resV01.PrimaryArtifact)
}
if resV01.AdvisorBrief == nil || resV01.AdvisorBrief.MaturityLevel != "V0.1" {
@@ -195,7 +196,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) {
keys, resV05.AdvisorBrief.MaturityScore, resV05.AdvisorBrief.MaturityLevel)
// 验证 feasibility_reg 受控文档已建立
docID := "doc_proj1001_feasibility_reg"
docID := "doc_proj1001_bd_feasibility_reg"
docRec, err := bizdev.Snapshot(svc).Gov.GetDocument(ctx, docID)
if err != nil || docRec == nil {
t.Fatalf("get docgov record failed: %v", err)
@@ -282,7 +283,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) {
t.Fatalf("Stage V0.5 force re-run should succeed, error: %s", resV05Updated.ErrorReason)
}
for _, art := range resV05Updated.SubArtifacts {
if art.TaskKey == "feasibility_reg" {
if workflowdef.SameTaskKey(art.TaskKey, "feasibility_reg") {
_ = bizdev.Snapshot(svc).Gov.SetDocumentWorkDir(ctx, docID, art.WorkDir)
break
}
@@ -362,7 +363,7 @@ func TestBizDev_FullLink_V01_V05_V10_E2E(t *testing.T) {
if !resV10.Success {
t.Fatalf("Stage V1.0 should succeed, error: %s", resV10.ErrorReason)
}
if resV10.PrimaryArtifact == nil || resV10.PrimaryArtifact.TaskKey != "cdo_quote" {
if resV10.PrimaryArtifact == nil || !workflowdef.SameTaskKey(resV10.PrimaryArtifact.TaskKey, "cdo_quote") {
t.Fatalf("Stage V1.0 primary artifact should be cdo_quote")
}
+20 -11
View File
@@ -220,7 +220,7 @@ func NewService(cfg *config.Config) (*Service, error) {
}
}
if pwSvc != nil && doc.TaskKey == "system_reqs" {
if pwSvc != nil && workflowdef.SameTaskKey(doc.TaskKey, "system_reqs") {
_, _ = pwSvc.CheckAndPropagateStale(ctx, doc.DocID, doc.ContentSHA256, fmt.Sprintf("上游系统需求规格书 V%d 已正式批准,已标记关联下游验证方案为待复核(stale)", doc.Version))
}
})
@@ -285,22 +285,31 @@ var SupportedStages = []StageMeta{
Stage: StageV01_Triage,
Title: "V0.1 闪电摸底与盲区探测",
Description: "快速梳理客户诉求与约束,建立基准技术假设,生成首期澄清微信话术与初始需求表",
Tasks: []string{"intake"},
Tasks: []string{"bd_intake"},
},
{
Stage: StageV05_Feasibility,
Title: "V0.5 方案成型与法规对齐",
Description: "法规与注册路径初筛、竞品调研、系统初步架构设计与关键选型建议",
Tasks: []string{"research", "feasibility_dev", "feasibility_reg", "user_requirements", "prelim_design"},
Tasks: []string{"bd_research", "bd_feasibility_dev", "bd_feasibility_reg", "bd_user_requirements", "bd_prelim_design"},
},
{
Stage: StageV10_Quoting,
Title: "V1.0 工程拆解与商业锁定",
Description: "全维度 WBS 工时拆解、Excel 定价核算、正式 CDO 商务报价单与谈判防御条款建议",
Tasks: []string{"cdo_quote"},
Tasks: []string{"bd_cdo_quote"},
},
}
func taskIs(key string, wants ...string) bool {
for _, want := range wants {
if workflowdef.SameTaskKey(key, want) {
return true
}
}
return false
}
func (s *Service) ListTasks() []TaskMeta {
return s.squad.ListTasks()
}
@@ -311,7 +320,7 @@ func resolveDocStage(req TaskRunRequest) string {
if sk := strings.TrimSpace(req.StageKey); sk != "" {
return sk
}
switch strings.TrimSpace(req.TaskKey) {
switch workflowdef.ImplementationTaskKey(strings.TrimSpace(req.TaskKey)) {
case "intake":
return string(StageV01_Triage)
case "research", "feasibility_dev", "feasibility_reg", "user_requirements", "prelim_design":
@@ -390,7 +399,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
}
// 针对 system_reqs(智能产品经理):若未显式提供 materials,自动抽取项目事实黑板/集体上下文为需求输入
if req.TaskKey == "system_reqs" && len(materials) == 0 && s.ctxMgr != nil && req.ProjectID > 0 {
if workflowdef.SameTaskKey(req.TaskKey, "system_reqs") && len(materials) == 0 && s.ctxMgr != nil && req.ProjectID > 0 {
coll, err := s.getProjectContextForUser(ctx, req.ProjectID, req.UserID)
if err == nil && coll != nil {
var b strings.Builder
@@ -437,7 +446,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
// 自动补充知识库检索 (如针对 research、feasibility_reg、cleanroom_spec、prelim_design、system_reqs、test_manager 等阶段)
hits := req.KnowledgeHits
if len(hits) == 0 && (req.TaskKey == "research" || req.TaskKey == "feasibility_reg" || req.TaskKey == "cleanroom_spec" || req.TaskKey == "prelim_design" || req.TaskKey == "system_reqs" || req.TaskKey == "test_manager") {
if len(hits) == 0 && taskIs(req.TaskKey, "research", "feasibility_reg", "cleanroom_spec", "prelim_design", "system_reqs", "test_manager") {
device := req.DeviceType
if device == "" && req.Fields != nil {
device = req.Fields["device_type"]
@@ -490,7 +499,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
}
// 智能测试工程师:统一项目上下文快照;SRS 仅为可选高权威来源
var upstreamSRDoc *docgov.DocumentRecord
if req.TaskKey == "test_manager" || req.TaskKey == "system_reqs" {
if taskIs(req.TaskKey, "test_manager", "system_reqs") {
priorDocs := make([]squad.PriorDocItem, 0)
facts := map[string]string{}
if req.Fields != nil {
@@ -554,7 +563,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
}
}
// 优先注入已批准 SR(PM 或 rd_system_reqs)
for _, key := range []string{"system_reqs", "rd_system_reqs"} {
for _, key := range []string{"system_reqs", "pm_system_reqs", "rd_system_reqs"} {
docID := fmt.Sprintf("doc_proj%d_%s", req.ProjectID, key)
if s.docGov == nil {
continue
@@ -1157,7 +1166,7 @@ func (s *Service) finalizeSquadDeliverable(
map[string]any{"task_key": res.TaskKey, "work_dir": res.WorkDir}, true)
}
if res.Success && req.TaskKey == "test_manager" && upstreamSRDoc != nil && s.projectWorkflow != nil && req.ProjectID > 0 {
if res.Success && workflowdef.SameTaskKey(req.TaskKey, "test_manager") && upstreamSRDoc != nil && s.projectWorkflow != nil && req.ProjectID > 0 {
_ = s.projectWorkflow.RecordLineage(ctx, &projectworkflow.DocumentLineage{
ProjectID: req.ProjectID,
DownstreamDocID: docID,
@@ -1169,7 +1178,7 @@ func (s *Service) finalizeSquadDeliverable(
})
}
// Stale propagation only after fact source committed (not while pending confirmation).
if res.Success && req.TaskKey == "system_reqs" && s.projectWorkflow != nil && req.ProjectID > 0 &&
if res.Success && workflowdef.SameTaskKey(req.TaskKey, "system_reqs") && s.projectWorkflow != nil && req.ProjectID > 0 &&
(s.docRev == nil || (docRec != nil && docRec.ContentSHA256 != "")) {
sha := sha256.Sum256([]byte(res.MarkdownBody))
shaStr := hex.EncodeToString(sha[:])
+5 -4
View File
@@ -10,6 +10,7 @@ import (
"chat-agent-system/internal/config"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/workflowdef"
)
func TestClassifyStageTaskHalt_QualityFailedStopsWithoutAbsorbing(t *testing.T) {
@@ -77,12 +78,12 @@ func TestExecuteStage_QualityFailed_StopsChainWithoutAbsorbingFactsOrPriorDoc(t
var seenKeys []string
svc.stageTaskHook = func(_ context.Context, req TaskRunRequest) (*TaskRunResult, error) {
seenKeys = append(seenKeys, req.TaskKey)
if req.TaskKey != "research" {
if !workflowdef.SameTaskKey(req.TaskKey, "research") {
t.Fatalf("quality fail must stop before subsequent task %q; seen=%v priors=%d",
req.TaskKey, seenKeys, len(req.PriorDocs))
}
for _, p := range req.PriorDocs {
if p.DocKey == "research" || strings.Contains(p.Content, "未过门禁") {
if workflowdef.SameTaskKey(p.DocKey, "research") || strings.Contains(p.Content, "未过门禁") {
t.Fatalf("must not feed failed markdown as PriorDoc: %+v", p)
}
}
@@ -135,10 +136,10 @@ func TestExecuteStage_QualityFailed_StopsChainWithoutAbsorbingFactsOrPriorDoc(t
if stageRes.PrimaryArtifact.TerminalStatus() != TaskStatusQualityFailed {
t.Fatalf("primary status=%q", stageRes.PrimaryArtifact.TerminalStatus())
}
if len(stageRes.SubArtifacts) != 1 || seenKeys[0] != "research" || len(seenKeys) != 1 {
if len(stageRes.SubArtifacts) != 1 || !workflowdef.SameTaskKey(seenKeys[0], "research") || len(seenKeys) != 1 {
t.Fatalf("must stop after first task; sub=%d seen=%v", len(stageRes.SubArtifacts), seenKeys)
}
for _, next := range []string{"feasibility_dev", "feasibility_reg", "user_requirements", "prelim_design"} {
for _, next := range []string{"feasibility_dev", "bd_feasibility_dev", "feasibility_reg", "bd_feasibility_reg", "user_requirements", "bd_user_requirements", "prelim_design", "bd_prelim_design"} {
if _, err := os.Stat(filepath.Join(stageWork, next, "output.md")); err == nil {
t.Fatalf("subsequent task %s must not run", next)
}
@@ -88,7 +88,7 @@ func TestPG_ActivateOfficialWorkflowRuntimeMetadata(t *testing.T) {
SELECT dti.executor_key
FROM project_doc_task_instances dti
JOIN project_stage_instances si ON si.id = dti.stage_instance_id
WHERE si.workflow_instance_id = $1 AND dti.task_key = 'intake'
WHERE si.workflow_instance_id = $1 AND dti.task_key IN ('intake', 'bd_intake')
`, activated.Instance.ID).Scan(&executorKey)
if err != nil {
t.Fatalf("query runtime metadata: %v", err)
@@ -96,7 +96,7 @@ func TestPG_ActivationAndStageAdvance(t *testing.T) {
}
for i := range plan.Stages {
for j := range plan.Stages[i].DocTasks {
if plan.Stages[i].DocTasks[j].TaskKey == "intake" {
if plan.Stages[i].DocTasks[j].TaskKey == "intake" || plan.Stages[i].DocTasks[j].TaskKey == "bd_intake" {
plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{
AuthorUserID: 2, ReviewerUserID: 3, ApproverUserID: 4,
}
+17 -8
View File
@@ -2,15 +2,17 @@ package projectinstance
import (
"context"
"chat-agent-system/internal/workflowdef"
)
// StageAccess 用户在某项目实例下可访问的阶段与文档任务范围。
type StageAccess struct {
Restricted bool `json:"restricted"`
ActiveStageKey string `json:"active_stage_key,omitempty"`
AllowedStageKeys map[string]bool `json:"allowed_stage_keys,omitempty"`
AllowedTaskKeys map[string]bool `json:"allowed_task_keys,omitempty"`
ViewableTaskKeys map[string]bool `json:"viewable_task_keys,omitempty"`
Restricted bool `json:"restricted"`
ActiveStageKey string `json:"active_stage_key,omitempty"`
AllowedStageKeys map[string]bool `json:"allowed_stage_keys,omitempty"`
AllowedTaskKeys map[string]bool `json:"allowed_task_keys,omitempty"`
ViewableTaskKeys map[string]bool `json:"viewable_task_keys,omitempty"`
}
// ResolveStageAccess 基于治理实例与三权指派计算阶段 ABAC 范围。
@@ -129,7 +131,14 @@ func (a *StageAccess) TaskKeyAllowed(taskKey string) bool {
if a == nil || !a.Restricted {
return true
}
return a.AllowedTaskKeys[taskKey]
return mapHasTaskKey(a.AllowedTaskKeys, taskKey)
}
func mapHasTaskKey(keys map[string]bool, taskKey string) bool {
if keys[taskKey] || keys[workflowdef.CanonicalTaskKey(taskKey)] || keys[workflowdef.ImplementationTaskKey(taskKey)] {
return true
}
return false
}
// TaskKeyVisible 判断文档是否对项目成员可见(view 豁免阶段收窄)。
@@ -138,9 +147,9 @@ func (a *StageAccess) TaskKeyVisible(taskKey string) bool {
return true
}
if len(a.ViewableTaskKeys) == 0 {
return a.AllowedTaskKeys[taskKey]
return mapHasTaskKey(a.AllowedTaskKeys, taskKey)
}
return a.ViewableTaskKeys[taskKey]
return mapHasTaskKey(a.ViewableTaskKeys, taskKey)
}
// StageKeyAllowed 判断 stage_key 是否在允许范围内。
+4 -4
View File
@@ -85,8 +85,8 @@ func NewDefaultCatalog() Catalog {
Name: "软件项目自举流程",
Description: "S0–S4:章程、现状、需求、架构、风险、路线与就绪评审",
StageKey: "S0_boundary",
TaskKey: "software_charter",
OutputDocType: "software_charter",
TaskKey: "sw_software_charter",
OutputDocType: "sw_software_charter",
OutputDocTitle: "项目章程",
Enabled: true,
}
@@ -96,8 +96,8 @@ func NewDefaultCatalog() Catalog {
Name: "软件交付迭代流程",
Description: "D0–D4:变更定义、实施证据、验证、发布与复盘",
StageKey: "D0_change_definition",
TaskKey: "change_brief",
OutputDocType: "change_brief",
TaskKey: "sw_change_brief",
OutputDocType: "sw_change_brief",
OutputDocTitle: "变更简报",
Enabled: true,
}
@@ -92,12 +92,12 @@ func TestWorkflowGovernance_PiAgent_PG_FullNoneMixedChain(t *testing.T) {
for j := range stage.DocTasks {
dt := &stage.DocTasks[j]
switch {
case stage.StageKey == "V0.1_triage" && dt.TaskKey == "intake":
case stage.StageKey == "V0.1_triage" && (dt.TaskKey == "intake" || dt.TaskKey == "bd_intake"):
dt.Enabled = true
dt.Duties = projectinstance.DocDuty{
AuthorUserID: creatorID, ReviewerUserID: reviewerID, ApproverUserID: approverID,
}
case stage.StageKey == "V0.5_feasibility" && dt.TaskKey == "research":
case stage.StageKey == "V0.5_feasibility" && (dt.TaskKey == "research" || dt.TaskKey == "bd_research"):
dt.Enabled = true
dt.Duties = projectinstance.DocDuty{} // none
default:
@@ -265,7 +265,7 @@ func TestWorkflowGovernance_PiAgent_PG_FullNoneMixedChain(t *testing.T) {
}
var researchItem *projectinstance.DocTaskReadiness
for i := range readiness.DocTasks {
if readiness.DocTasks[i].TaskKey == "research" {
if readiness.DocTasks[i].TaskKey == "research" || readiness.DocTasks[i].TaskKey == "bd_research" {
researchItem = &readiness.DocTasks[i]
break
}
+3 -1
View File
@@ -9,6 +9,8 @@ import (
"path/filepath"
"strings"
"unicode/utf8"
"chat-agent-system/internal/workflowdef"
)
// ExtractTextWithAnchors 从不同格式材料中提取纯文本,并自动注入物理页码锚点 <!-- page N -->
@@ -230,6 +232,7 @@ func ExtractAdvisorBriefing(taskKey string, markdownBody string, fields map[stri
}
// 2. 根据任务类型与提取事实定制成熟度、风险与话术
taskKey = workflowdef.ImplementationTaskKey(taskKey)
switch taskKey {
case "intake", "user_requirements":
brief.MaturityScore = 65
@@ -343,4 +346,3 @@ func ExtractAdvisorBriefing(taskKey string, markdownBody string, fields map[stri
return brief
}
+20
View File
@@ -0,0 +1,20 @@
package squad
import "testing"
func TestDefaultWorkflowSteps_CanonicalAlias(t *testing.T) {
legacy := DefaultWorkflowSteps("intake")
canonical := DefaultWorkflowSteps("bd_intake")
if len(legacy) == 0 || len(legacy) != len(canonical) {
t.Fatalf("intake steps %d, bd_intake steps %d", len(legacy), len(canonical))
}
if legacy[0].Role != canonical[0].Role {
t.Fatalf("alias diverged: %s vs %s", legacy[0].Role, canonical[0].Role)
}
if TaskArtifactTitle("intake") != TaskArtifactTitle("bd_intake") {
t.Fatal("artifact title must match across the alias")
}
if !isStandardTask("tm_system_verification") || !isStandardTask("test_manager") {
t.Fatal("test manager alias must stay on the whitelist")
}
}
+1 -6
View File
@@ -55,12 +55,7 @@ func (p *AdaptivePlanner) Plan(req PlannerRequest) (*PlannerResult, error) {
}
func isStandardTask(taskKey string) bool {
for _, t := range SupportedTasks {
if t.Key == taskKey {
return true
}
}
return false
return taskKnown(taskKey)
}
// planFixedAdaptive 固定工作流动态微调
+4
View File
@@ -16,6 +16,7 @@ import (
"chat-agent-system/internal/agent"
"chat-agent-system/internal/stream"
"chat-agent-system/internal/workflowdef"
)
// ProhibitedPhrases 医疗器械文档小队严禁出现的敷衍套话与省略词
@@ -97,6 +98,9 @@ func (q *QualityGate) Validate(wsDir string, taskKey string, content string) (*Q
report.Passed = false
}
// 新键与旧键共用同一套门禁;rd_* 不在别名表里,不会被改写。
taskKey = workflowdef.ImplementationTaskKey(taskKey)
// 4. CDO 报价任务检查
if taskKey == "cdo_quote" {
xlsxPath := filepath.Join(wsDir, "CDO报价核算-V01.xlsx")
+24 -12
View File
@@ -13,6 +13,7 @@ import (
"chat-agent-system/internal/config"
"chat-agent-system/internal/quote"
"chat-agent-system/internal/stream"
"chat-agent-system/internal/workflowdef"
)
// TaskMeta 任务元信息
@@ -126,15 +127,15 @@ func NewService(cfg *config.Config) *Service {
}
var baseSupportedTasks = []TaskMeta{
{Key: "intake", Title: "用户初始需求收集", ArtifactTitle: "用户初始需求收集表", Description: "梳理客户原始需求、提炼事实清单与缺口"},
{Key: "research", Title: "调研", ArtifactTitle: "调研报告", Description: "市场竞品同维度对比、法规要点与技术方案初筛"},
{Key: "feasibility_dev", Title: "设计开发可行性评估", ArtifactTitle: "设计开发可行性评估报告", Description: "工程化差距、技术路线与系统架构可行性推导"},
{Key: "feasibility_reg", Title: "注册路径可行性评估", ArtifactTitle: "注册路径可行性评估报告", Description: "分类建议、临床评价策略与主要标准检测规划"},
{Key: "user_requirements", Title: "用户需求表", ArtifactTitle: "用户需求表", Description: "用户角色画像与可追溯需求条目清单"},
{Key: "system_reqs", Title: "系统需求规格书", ArtifactTitle: "系统需求规格书", Description: "智能产品经理深度提炼,输出系统需求规格书及工程追溯"},
{Key: "test_manager", Title: "系统验证方案及计划", ArtifactTitle: "系统验证方案及计划", Description: "智能测试经理基于系统需求生成系统测试方案与测试用例"},
{Key: "prelim_design", Title: "系统初步设计方案", ArtifactTitle: "系统初步设计方案", Description: "子系统架构边界、数据接口与Mermaid上下文流向"},
{Key: "cdo_quote", Title: "CDO报价单", ArtifactTitle: "CDO报价单(含定价核算 Excel)", Description: "基于公司定价模型的工时拆解、成本核算与法定说明"},
{Key: "bd_intake", Title: "用户初始需求收集", ArtifactTitle: "用户初始需求收集表", Description: "梳理客户原始需求、提炼事实清单与缺口"},
{Key: "bd_research", Title: "调研", ArtifactTitle: "调研报告", Description: "市场竞品同维度对比、法规要点与技术方案初筛"},
{Key: "bd_feasibility_dev", Title: "设计开发可行性评估", ArtifactTitle: "设计开发可行性评估报告", Description: "工程化差距、技术路线与系统架构可行性推导"},
{Key: "bd_feasibility_reg", Title: "注册路径可行性评估", ArtifactTitle: "注册路径可行性评估报告", Description: "分类建议、临床评价策略与主要标准检测规划"},
{Key: "bd_user_requirements", Title: "用户需求表", ArtifactTitle: "用户需求表", Description: "用户角色画像与可追溯需求条目清单"},
{Key: "pm_system_reqs", Title: "系统需求规格书", ArtifactTitle: "系统需求规格书", Description: "智能产品经理深度提炼,输出系统需求规格书及工程追溯"},
{Key: "tm_system_verification", Title: "系统验证方案及计划", ArtifactTitle: "系统验证方案及计划", Description: "智能测试经理基于系统需求生成系统测试方案与测试用例"},
{Key: "bd_prelim_design", Title: "系统初步设计方案", ArtifactTitle: "系统初步设计方案", Description: "子系统架构边界、数据接口与Mermaid上下文流向"},
{Key: "bd_cdo_quote", Title: "CDO报价单", ArtifactTitle: "CDO报价单(含定价核算 Excel)", Description: "基于公司定价模型的工时拆解、成本核算与法定说明"},
}
// SupportedTasks 标准小队任务白名单(商务九键 + rdev 六十键)。
@@ -148,9 +149,20 @@ func (s *Service) ListTasks() []TaskMeta {
return SupportedTasks
}
func TaskArtifactTitle(taskKey string) string {
func taskKnown(taskKey string) bool {
canon := workflowdef.CanonicalTaskKey(taskKey)
for _, t := range SupportedTasks {
if t.Key == taskKey {
if t.Key == canon {
return true
}
}
return false
}
func TaskArtifactTitle(taskKey string) string {
canon := workflowdef.CanonicalTaskKey(taskKey)
for _, t := range SupportedTasks {
if t.Key == canon {
return t.ArtifactTitle
}
}
@@ -231,7 +243,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
// 2. 若为 CDO 报价任务,先执行 Excel 报价核算引擎并回填
var attachments []Attachment
if req.TaskKey == "cdo_quote" {
if workflowdef.SameTaskKey(req.TaskKey, "cdo_quote") {
att, err := s.handleCDOQuotePreparation(workDir, req)
if err == nil && att != nil {
attachments = append(attachments, *att)
+6 -3
View File
@@ -9,6 +9,7 @@ import (
"time"
"chat-agent-system/internal/template"
"chat-agent-system/internal/workflowdef"
)
// WorkspacePrepareRequest 工作区准备请求
@@ -608,6 +609,7 @@ func DefaultSquadAgents(taskKey string) []AgentSpec {
if strings.HasPrefix(taskKey, "rd_") {
return rdevSquadAgents(taskKey, "")
}
taskKey = workflowdef.ImplementationTaskKey(taskKey)
switch taskKey {
case "intake":
return []AgentSpec{
@@ -722,7 +724,7 @@ func DefaultSquadAgents(taskKey string) []AgentSpec {
Model: "claude-3-7-sonnet",
Tools: []string{"read", "write", "edit"},
BashPermission: "deny",
Responsibilities: []string{"严格按 system_reqs.md 模板成文", "正文结构完整", "填满追溯表"},
Responsibilities: []string{"严格按 pm_system_reqs.md 模板成文", "正文结构完整", "填满追溯表"},
ForbiddenActions: []string{"严禁残留占位符", "严禁以省略写法跳过应写章节"},
},
{
@@ -770,7 +772,7 @@ func DefaultSquadAgents(taskKey string) []AgentSpec {
Model: "claude-3-7-sonnet",
Tools: []string{"read", "write", "edit"},
BashPermission: "deny",
Responsibilities: []string{"严格按 test_manager.md 模板成文", "用例主表翔实完整"},
Responsibilities: []string{"严格按 tm_system_verification.md 模板成文", "用例主表翔实完整"},
ForbiddenActions: []string{"严禁残留占位符", "严禁以省略写法跳过应写章节"},
},
{
@@ -818,6 +820,7 @@ func DefaultWorkflowSteps(taskKey string) []StepSpec {
if strings.HasPrefix(taskKey, "rd_") {
return rdevWorkflowSteps(taskKey, "")
}
taskKey = workflowdef.ImplementationTaskKey(taskKey)
switch taskKey {
case "intake":
return []StepSpec{
@@ -1099,7 +1102,7 @@ func DefaultWorkflowSteps(taskKey string) []StepSpec {
"tasks/03_regulatory/output.md",
},
Accept: []string{
"严格按 test_manager.md 模板生成正式方案文本",
"严格按 tm_system_verification.md 模板生成正式方案文本",
"嵌入测试用例清单与覆盖率统计,严禁遗漏上游需求",
},
},
+4 -4
View File
@@ -71,12 +71,12 @@ func TestGetDocTemplate_AppRootLayout(t *testing.T) {
if err := os.MkdirAll(dest, 0o755); err != nil {
t.Fatal(err)
}
src := filepath.Join(root, "templates", "docs", "prelim_design.md")
src := filepath.Join(root, "templates", "docs", "bd_prelim_design.md")
body, err := os.ReadFile(src)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dest, "prelim_design.md"), body, 0o644); err != nil {
if err := os.WriteFile(filepath.Join(dest, "bd_prelim_design.md"), body, 0o644); err != nil {
t.Fatal(err)
}
agentDir := filepath.Join(app, "configs", "agents")
@@ -132,10 +132,10 @@ func TestDockerBuildContext_TemplatesDocsReadable(t *testing.T) {
_ = exec.Command("docker", "rmi", "-f", tag).Run()
})
cat := exec.Command("docker", "run", "--rm", "--entrypoint", "cat", tag, "/app/templates/docs/prelim_design.md")
cat := exec.Command("docker", "run", "--rm", "--entrypoint", "cat", tag, "/app/templates/docs/bd_prelim_design.md")
body, err := runTimed(cat, 30*time.Second)
if err != nil {
t.Fatalf("image missing /app/templates/docs/prelim_design.md: %v\n%s", err, body)
t.Fatalf("image missing /app/templates/docs/bd_prelim_design.md: %v\n%s", err, body)
}
if !bytes.Contains(body, []byte("系统初步设计方案")) || !bytes.Contains(body, []byte("子系统划分")) {
t.Fatalf("image template is not the official prelim_design (%d bytes)", len(body))
+22 -8
View File
@@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"
"chat-agent-system/internal/workflowdef"
"chat-agent-system/templates"
)
@@ -42,16 +43,29 @@ func GetDocTemplate(name string) (string, error) {
if name == "" || strings.Contains(name, "..") || strings.ContainsAny(name, `/\`) {
return "", fmt.Errorf("read doc template: invalid name %q", name)
}
if b, err := templates.DocTemplates.ReadFile("docs/" + name + ".md"); err == nil && len(strings.TrimSpace(string(b))) > 0 {
return string(b), nil
for _, candidate := range templateNames(name) {
if b, err := templates.DocTemplates.ReadFile("docs/" + candidate + ".md"); err == nil && len(strings.TrimSpace(string(b))) > 0 {
return string(b), nil
}
root := findProjectRoot()
path := filepath.Join(root, "templates", "docs", candidate+".md")
b, err := os.ReadFile(path)
if err == nil && len(strings.TrimSpace(string(b))) > 0 {
return string(b), nil
}
}
root := findProjectRoot()
path := filepath.Join(root, "templates", "docs", name+".md")
b, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read doc template %s: %w", name, err)
return "", fmt.Errorf("read doc template %s: file not found", name)
}
func templateNames(name string) []string {
names := []string{name}
if canonical := workflowdef.CanonicalTaskKey(name); canonical != name {
names = append(names, canonical)
}
return string(b), nil
if legacy := workflowdef.ImplementationTaskKey(name); legacy != name {
names = append(names, legacy)
}
return names
}
func GetAgentInstruction(role string) (string, error) {
+98
View File
@@ -0,0 +1,98 @@
package workflowdef
import (
"regexp"
"strings"
)
// legacyToCanonical 是 D12 的旧任务键 → 新任务键。
// 冻结实例继续持有旧键;新发布的清单使用新键。两边必须指向同一套步骤和模板。
var legacyToCanonical = map[string]string{
"intake": "bd_intake",
"research": "bd_research",
"feasibility_dev": "bd_feasibility_dev",
"feasibility_reg": "bd_feasibility_reg",
"user_requirements": "bd_user_requirements",
"prelim_design": "bd_prelim_design",
"cdo_quote": "bd_cdo_quote",
"system_reqs": "pm_system_reqs",
"test_manager": "tm_system_verification",
"software_charter": "sw_software_charter",
"source_inventory": "sw_source_inventory",
"current_state_audit": "sw_current_state_audit",
"architecture_baseline": "sw_architecture_baseline",
"software_risk_register": "sw_software_risk_register",
"product_requirements": "sw_product_requirements",
"scope_backlog": "sw_scope_backlog",
"target_architecture": "sw_target_architecture",
"decision_log": "sw_decision_log",
"bootstrap_roadmap": "sw_bootstrap_roadmap",
"verification_strategy": "sw_verification_strategy",
"operations_baseline": "sw_operations_baseline",
"bootstrap_readiness": "sw_bootstrap_readiness",
"change_brief": "sw_change_brief",
"implementation_plan": "sw_implementation_plan",
"change_manifest": "sw_change_manifest",
"adr_delta": "sw_adr_delta",
"test_report": "sw_test_report",
"security_review": "sw_security_review",
"release_plan": "sw_release_plan",
"release_notes": "sw_release_notes",
"retrospective": "sw_retrospective",
"backlog_delta": "sw_backlog_delta",
}
var canonicalToLegacy map[string]string
func init() {
canonicalToLegacy = make(map[string]string, len(legacyToCanonical))
for legacy, canonical := range legacyToCanonical {
canonicalToLegacy[canonical] = legacy
}
}
// CanonicalTaskKey 把旧键收成发布用的新键。未知键(含 rd_*)原样返回。
func CanonicalTaskKey(key string) string {
key = strings.TrimSpace(key)
if canonical, ok := legacyToCanonical[key]; ok {
return canonical
}
return key
}
// ImplementationTaskKey 把新键收成现有步骤开关仍使用的旧键。
// 存量执行路径不改写;新键只是别名。
func ImplementationTaskKey(key string) string {
key = strings.TrimSpace(key)
if legacy, ok := canonicalToLegacy[key]; ok {
return legacy
}
return key
}
// SameTaskKey 判断两个键是否同一任务(旧键与新键视为相同)。
func SameTaskKey(a, b string) bool {
ca := CanonicalTaskKey(a)
cb := CanonicalTaskKey(b)
return ca != "" && ca == cb
}
var namespacedDocID = regexp.MustCompile(`^(doc_proj\d+_)(.+)$`)
// AlternateDocIDs 给出同一文档的另一种键名形式。
// 账本不批量迁移:旧 ID 仍可查,新 ID 也要能指回旧行。
func AlternateDocIDs(docID string) []string {
m := namespacedDocID.FindStringSubmatch(strings.TrimSpace(docID))
if len(m) != 3 {
return nil
}
prefix, suffix := m[1], m[2]
var out []string
if canonical := CanonicalTaskKey(suffix); canonical != suffix {
out = append(out, prefix+canonical)
}
if legacy := ImplementationTaskKey(suffix); legacy != suffix {
out = append(out, prefix+legacy)
}
return out
}
+93
View File
@@ -0,0 +1,93 @@
package workflowdef
import (
"strings"
"testing"
)
func TestSameTaskKey_LegacyAndCanonical(t *testing.T) {
pairs := [][2]string{
{"intake", "bd_intake"},
{"system_reqs", "pm_system_reqs"},
{"test_manager", "tm_system_verification"},
{"software_charter", "sw_software_charter"},
{"change_brief", "sw_change_brief"},
}
for _, pair := range pairs {
if !SameTaskKey(pair[0], pair[1]) {
t.Fatalf("expected %s == %s", pair[0], pair[1])
}
if CanonicalTaskKey(pair[0]) != pair[1] {
t.Fatalf("canonical %s = %s, want %s", pair[0], CanonicalTaskKey(pair[0]), pair[1])
}
if ImplementationTaskKey(pair[1]) != pair[0] {
t.Fatalf("implementation %s = %s, want %s", pair[1], ImplementationTaskKey(pair[1]), pair[0])
}
}
if SameTaskKey("intake", "research") {
t.Fatal("intake and research must stay distinct")
}
if CanonicalTaskKey("rd_proposal") != "rd_proposal" {
t.Fatal("rd_ keys are already namespaced")
}
}
func TestAlternateDocIDs_RoundTrip(t *testing.T) {
alts := AlternateDocIDs("doc_proj56_intake")
if len(alts) != 1 || alts[0] != "doc_proj56_bd_intake" {
t.Fatalf("old id aliases = %v", alts)
}
back := AlternateDocIDs("doc_proj56_bd_intake")
if len(back) != 1 || back[0] != "doc_proj56_intake" {
t.Fatalf("new id aliases = %v", back)
}
if got := AlternateDocIDs("doc_proj56_rd_proposal"); len(got) != 0 {
t.Fatalf("rd_ doc id should not alias, got %v", got)
}
}
func TestPublishedTaskKeysAreNamespacedAndDisjoint(t *testing.T) {
files := map[string]string{
"main.v2.json": "bd_",
"product_manager.v2.json": "pm_",
"test_manager.v2.json": "tm_",
"rdev.v2.json": "rd_",
"software_bootstrap.v2.json": "sw_",
"software_delivery.v2.json": "sw_",
}
seen := map[string]string{}
for file, prefix := range files {
def, err := LoadManifestDefinition(file)
if err != nil {
t.Fatal(err)
}
for _, stage := range def.Stages {
for _, task := range stage.DocTasks {
key := task.Key
if !strings.HasPrefix(key, prefix) {
t.Fatalf("%s task %s missing prefix %s", file, key, prefix)
}
if prev, ok := seen[key]; ok {
t.Fatalf("task key %s intersects %s and %s", key, prev, file)
}
seen[key] = file
if task.OutputDocType != key {
t.Fatalf("%s output_doc_type %s != key %s", file, task.OutputDocType, key)
}
if prefix == "rd_" {
if ImplementationTaskKey(key) != key {
t.Fatalf("rd_ key %s must not alias", key)
}
continue
}
legacy := ImplementationTaskKey(key)
if legacy == key {
t.Fatalf("%s has no legacy alias", key)
}
if CanonicalTaskKey(legacy) != key {
t.Fatalf("legacy %s does not round-trip to %s", legacy, key)
}
}
}
}
}
+85 -28
View File
@@ -1,24 +1,29 @@
{
"schema_version": 2,
"workflow_key": "main",
"product_domains": ["medical_device", "non_medical_software"],
"product_domains": [
"medical_device",
"non_medical_software"
],
"stages": [
{
"key": "V0.1_triage",
"name": "V0.1 需求摸底",
"position": 10,
"optional": false,
"doc_tasks": [{
"key": "intake",
"title": "用户初始需求收集表",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.intake.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "intake",
"output_doc_title": "用户初始需求收集表"
}]
"doc_tasks": [
{
"key": "bd_intake",
"title": "用户初始需求收集表",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.intake.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bd_intake",
"output_doc_title": "用户初始需求收集表"
}
]
},
{
"key": "V0.5_feasibility",
@@ -26,11 +31,61 @@
"position": 20,
"optional": false,
"doc_tasks": [
{"key": "research", "title": "调研报告", "default_enabled": true, "creator_configurable": true, "blocking_when_enabled": true, "executor_key": "bizdev.research.v1", "quality_gate_key": "quality.generic.v1", "output_doc_type": "research", "output_doc_title": "调研报告"},
{"key": "feasibility_dev", "title": "设计开发可行性评估报告", "default_enabled": true, "creator_configurable": true, "blocking_when_enabled": true, "executor_key": "bizdev.feasibility_dev.v1", "quality_gate_key": "quality.generic.v1", "output_doc_type": "feasibility_dev", "output_doc_title": "设计开发可行性评估报告"},
{"key": "feasibility_reg", "title": "注册路径可行性评估报告", "default_enabled": true, "creator_configurable": true, "blocking_when_enabled": true, "executor_key": "bizdev.feasibility.v1", "quality_gate_key": "quality.generic.v1", "output_doc_type": "feasibility_reg", "output_doc_title": "注册路径可行性评估报告"},
{"key": "user_requirements", "title": "用户需求表", "default_enabled": true, "creator_configurable": true, "blocking_when_enabled": true, "executor_key": "bizdev.user_requirements.v1", "quality_gate_key": "quality.generic.v1", "output_doc_type": "user_requirements", "output_doc_title": "用户需求表"},
{"key": "prelim_design", "title": "系统初步设计方案", "default_enabled": true, "creator_configurable": true, "blocking_when_enabled": true, "executor_key": "bizdev.prelim_design.v1", "quality_gate_key": "quality.generic.v1", "output_doc_type": "prelim_design", "output_doc_title": "系统初步设计方案"}
{
"key": "bd_research",
"title": "调研报告",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.research.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bd_research",
"output_doc_title": "调研报告"
},
{
"key": "bd_feasibility_dev",
"title": "设计开发可行性评估报告",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.feasibility_dev.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bd_feasibility_dev",
"output_doc_title": "设计开发可行性评估报告"
},
{
"key": "bd_feasibility_reg",
"title": "注册路径可行性评估报告",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.feasibility.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bd_feasibility_reg",
"output_doc_title": "注册路径可行性评估报告"
},
{
"key": "bd_user_requirements",
"title": "用户需求表",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.user_requirements.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bd_user_requirements",
"output_doc_title": "用户需求表"
},
{
"key": "bd_prelim_design",
"title": "系统初步设计方案",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.prelim_design.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bd_prelim_design",
"output_doc_title": "系统初步设计方案"
}
]
},
{
@@ -38,17 +93,19 @@
"name": "V1.0 商务报价",
"position": 30,
"optional": false,
"doc_tasks": [{
"key": "cdo_quote",
"title": "CDO 报价单",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.cdo_quote.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "cdo_quote",
"output_doc_title": "CDO 报价单"
}]
"doc_tasks": [
{
"key": "bd_cdo_quote",
"title": "CDO 报价单",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "bizdev.cdo_quote.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bd_cdo_quote",
"output_doc_title": "CDO 报价单"
}
]
}
]
}
@@ -1,22 +1,29 @@
{
"schema_version": 2,
"workflow_key": "product_manager",
"product_domains": ["medical_device", "non_medical_software"],
"stages": [{
"key": "requirements_baseline",
"name": "系统需求基线",
"position": 10,
"optional": false,
"doc_tasks": [{
"key": "system_reqs",
"title": "系统需求规格书",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "squad.system_reqs.v1",
"quality_gate_key": "quality.system_reqs.v2",
"output_doc_type": "system_reqs",
"output_doc_title": "系统需求规格书"
}]
}]
"product_domains": [
"medical_device",
"non_medical_software"
],
"stages": [
{
"key": "requirements_baseline",
"name": "系统需求基线",
"position": 10,
"optional": false,
"doc_tasks": [
{
"key": "pm_system_reqs",
"title": "系统需求规格书",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "squad.system_reqs.v1",
"quality_gate_key": "quality.system_reqs.v2",
"output_doc_type": "pm_system_reqs",
"output_doc_title": "系统需求规格书"
}
]
}
]
}
@@ -1,7 +1,9 @@
{
"schema_version": 2,
"workflow_key": "software_bootstrap",
"product_domains": ["non_medical_software"],
"product_domains": [
"non_medical_software"
],
"stages": [
{
"key": "S0_boundary",
@@ -11,25 +13,25 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "software_charter",
"key": "sw_software_charter",
"title": "项目章程",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "software_charter",
"output_doc_type": "sw_software_charter",
"output_doc_title": "项目章程"
},
{
"key": "source_inventory",
"key": "sw_source_inventory",
"title": "资料与仓库清单",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "source_inventory",
"output_doc_type": "sw_source_inventory",
"output_doc_title": "资料与仓库清单"
}
]
@@ -42,36 +44,36 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "current_state_audit",
"key": "sw_current_state_audit",
"title": "现状审计",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "current_state_audit",
"output_doc_type": "sw_current_state_audit",
"output_doc_title": "现状审计"
},
{
"key": "architecture_baseline",
"key": "sw_architecture_baseline",
"title": "当前架构与依赖图",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "architecture_baseline",
"output_doc_type": "sw_architecture_baseline",
"output_doc_title": "当前架构与依赖图"
},
{
"key": "software_risk_register",
"key": "sw_software_risk_register",
"title": "风险登记册",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "software_risk_register",
"output_doc_type": "sw_software_risk_register",
"output_doc_title": "风险登记册"
}
]
@@ -84,25 +86,25 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "product_requirements",
"key": "sw_product_requirements",
"title": "产品与系统需求基线",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "product_requirements",
"output_doc_type": "sw_product_requirements",
"output_doc_title": "产品与系统需求基线"
},
{
"key": "scope_backlog",
"key": "sw_scope_backlog",
"title": "范围化 backlog",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "scope_backlog",
"output_doc_type": "sw_scope_backlog",
"output_doc_title": "范围化 backlog"
}
]
@@ -115,25 +117,25 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "target_architecture",
"key": "sw_target_architecture",
"title": "目标架构方案",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "target_architecture",
"output_doc_type": "sw_target_architecture",
"output_doc_title": "目标架构方案"
},
{
"key": "decision_log",
"key": "sw_decision_log",
"title": "架构决策记录",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "decision_log",
"output_doc_type": "sw_decision_log",
"output_doc_title": "架构决策记录"
}
]
@@ -146,47 +148,47 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "bootstrap_roadmap",
"key": "sw_bootstrap_roadmap",
"title": "分阶段建设路线",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bootstrap_roadmap",
"output_doc_type": "sw_bootstrap_roadmap",
"output_doc_title": "分阶段建设路线"
},
{
"key": "verification_strategy",
"key": "sw_verification_strategy",
"title": "验证策略与测试矩阵",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "verification_strategy",
"output_doc_type": "sw_verification_strategy",
"output_doc_title": "验证策略与测试矩阵"
},
{
"key": "operations_baseline",
"key": "sw_operations_baseline",
"title": "部署运维与恢复基线",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "operations_baseline",
"output_doc_type": "sw_operations_baseline",
"output_doc_title": "部署运维与恢复基线"
},
{
"key": "bootstrap_readiness",
"key": "sw_bootstrap_readiness",
"title": "自举就绪评审",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bootstrap_readiness",
"output_doc_type": "sw_bootstrap_readiness",
"output_doc_title": "自举就绪评审"
}
]
@@ -1,7 +1,9 @@
{
"schema_version": 2,
"workflow_key": "software_delivery",
"product_domains": ["non_medical_software"],
"product_domains": [
"non_medical_software"
],
"required_upstream_workflow": "",
"stages": [
{
@@ -12,25 +14,25 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "change_brief",
"key": "sw_change_brief",
"title": "变更简报",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "change_brief",
"output_doc_type": "sw_change_brief",
"output_doc_title": "变更简报"
},
{
"key": "implementation_plan",
"key": "sw_implementation_plan",
"title": "实施计划",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "implementation_plan",
"output_doc_type": "sw_implementation_plan",
"output_doc_title": "实施计划"
}
]
@@ -43,25 +45,25 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "change_manifest",
"key": "sw_change_manifest",
"title": "变更清单",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "change_manifest",
"output_doc_type": "sw_change_manifest",
"output_doc_title": "变更清单"
},
{
"key": "adr_delta",
"key": "sw_adr_delta",
"title": "架构决策增量",
"default_enabled": false,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "adr_delta",
"output_doc_type": "sw_adr_delta",
"output_doc_title": "架构决策增量"
}
]
@@ -74,25 +76,25 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "test_report",
"key": "sw_test_report",
"title": "测试报告",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "test_report",
"output_doc_type": "sw_test_report",
"output_doc_title": "测试报告"
},
{
"key": "security_review",
"key": "sw_security_review",
"title": "安全审查",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "security_review",
"output_doc_type": "sw_security_review",
"output_doc_title": "安全审查"
}
]
@@ -105,25 +107,25 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "release_plan",
"key": "sw_release_plan",
"title": "发布计划",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "release_plan",
"output_doc_type": "sw_release_plan",
"output_doc_title": "发布计划"
},
{
"key": "release_notes",
"key": "sw_release_notes",
"title": "发布说明",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "release_notes",
"output_doc_type": "sw_release_notes",
"output_doc_title": "发布说明"
}
]
@@ -136,25 +138,25 @@
"manual_gate": true,
"doc_tasks": [
{
"key": "retrospective",
"key": "sw_retrospective",
"title": "复盘",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "retrospective",
"output_doc_type": "sw_retrospective",
"output_doc_title": "复盘"
},
{
"key": "backlog_delta",
"key": "sw_backlog_delta",
"title": "Backlog 增量",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "backlog_delta",
"output_doc_type": "sw_backlog_delta",
"output_doc_title": "Backlog 增量"
}
]
@@ -1,22 +1,29 @@
{
"schema_version": 2,
"workflow_key": "test_manager",
"product_domains": ["medical_device", "non_medical_software"],
"stages": [{
"key": "system_verification_planning",
"name": "系统验证策划",
"position": 10,
"optional": false,
"doc_tasks": [{
"key": "test_manager",
"title": "系统验证方案及计划",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "squad.test_manager.v1",
"quality_gate_key": "quality.test_manager.v2",
"output_doc_type": "test_manager",
"output_doc_title": "系统验证方案及计划"
}]
}]
"product_domains": [
"medical_device",
"non_medical_software"
],
"stages": [
{
"key": "system_verification_planning",
"name": "系统验证策划",
"position": 10,
"optional": false,
"doc_tasks": [
{
"key": "tm_system_verification",
"title": "系统验证方案及计划",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "squad.test_manager.v1",
"quality_gate_key": "quality.test_manager.v2",
"output_doc_type": "tm_system_verification",
"output_doc_title": "系统验证方案及计划"
}
]
}
]
}