package piplan import ( "context" "encoding/json" "fmt" "strings" "laodingbot/internal/logger" ) // Feature 产品经理视角输出的业务特性。 type Feature struct { FeatureID string `json:"feature_id"` Title string `json:"title"` BenefitHypothesis string `json:"benefit_hypothesis"` AcceptanceCriteria []string `json:"acceptance_criteria"` } // Enabler 系统架构师视角输出的技术赋能特性(架构跑道)。 type Enabler struct { EnablerID string `json:"enabler_id"` Title string `json:"title"` ArchitecturalPurpose string `json:"architectural_purpose"` } // NFRs 非功能性需求。 type NFRs struct { Performance string `json:"performance"` Security string `json:"security"` } // Dependency RTE 梳理的任务依赖关系。 type Dependency struct { SourceID string `json:"source_id"` TargetID string `json:"target_id"` Reason string `json:"reason"` } // PIPlanInput publish_pi_plan 工具的完整输入结构。 type PIPlanInput struct { PIVision string `json:"pi_vision"` Features []Feature `json:"features"` Enablers []Enabler `json:"enablers"` NFRs NFRs `json:"nfrs"` Dependencies []Dependency `json:"dependencies"` } // Tool 实现 SAFe PI 规划发布工具。 type Tool struct { maxOutputChars int log *logger.Logger } // New 创建一个新的 publish_pi_plan 工具实例。 func New(maxOutputChars int, log *logger.Logger) *Tool { if maxOutputChars <= 0 { maxOutputChars = 20000 } return &Tool{ maxOutputChars: maxOutputChars, log: log, } } func (t *Tool) Name() string { return "publish_pi_plan" } func (t *Tool) Description() string { return `当铁三角(PM, 架构师, RTE)完成 PI 规划推演后,调用此工具输出标准化的架构蓝图与任务清单。输入为 JSON,包含 pi_vision, features, enablers, nfrs, dependencies 字段。` } func (t *Tool) Call(ctx context.Context, input string) (string, error) { plan, err := parseInput(input) if err != nil { return "", fmt.Errorf("publish_pi_plan: invalid input: %w", err) } if err := validate(plan); err != nil { return "", fmt.Errorf("publish_pi_plan: validation failed: %w", err) } if t.log != nil { t.log.Infof("publish_pi_plan: features=%d enablers=%d deps=%d", len(plan.Features), len(plan.Enablers), len(plan.Dependencies)) } output := render(plan) if len([]rune(output)) > t.maxOutputChars { output = string([]rune(output)[:t.maxOutputChars]) } return output, nil } func parseInput(input string) (*PIPlanInput, error) { raw := strings.TrimSpace(input) if raw == "" { return nil, fmt.Errorf("empty input") } var plan PIPlanInput if err := json.Unmarshal([]byte(raw), &plan); err != nil { return nil, fmt.Errorf("JSON parse error: %w", err) } return &plan, nil } func validate(p *PIPlanInput) error { if strings.TrimSpace(p.PIVision) == "" { return fmt.Errorf("pi_vision is required") } if len(p.Features) == 0 { return fmt.Errorf("at least one feature is required") } for i, f := range p.Features { if strings.TrimSpace(f.FeatureID) == "" { return fmt.Errorf("features[%d].feature_id is required", i) } if strings.TrimSpace(f.Title) == "" { return fmt.Errorf("features[%d].title is required", i) } if strings.TrimSpace(f.BenefitHypothesis) == "" { return fmt.Errorf("features[%d].benefit_hypothesis is required", i) } if len(f.AcceptanceCriteria) == 0 { return fmt.Errorf("features[%d].acceptance_criteria requires at least one item", i) } } for i, e := range p.Enablers { if strings.TrimSpace(e.EnablerID) == "" { return fmt.Errorf("enablers[%d].enabler_id is required", i) } if strings.TrimSpace(e.Title) == "" { return fmt.Errorf("enablers[%d].title is required", i) } if strings.TrimSpace(e.ArchitecturalPurpose) == "" { return fmt.Errorf("enablers[%d].architectural_purpose is required", i) } } if strings.TrimSpace(p.NFRs.Performance) == "" { return fmt.Errorf("nfrs.performance is required") } if strings.TrimSpace(p.NFRs.Security) == "" { return fmt.Errorf("nfrs.security is required") } for i, d := range p.Dependencies { if strings.TrimSpace(d.SourceID) == "" { return fmt.Errorf("dependencies[%d].source_id is required", i) } if strings.TrimSpace(d.TargetID) == "" { return fmt.Errorf("dependencies[%d].target_id is required", i) } } return nil } // render 将 PI 规划输入渲染为标准化的 Markdown 架构蓝图与任务清单。 func render(p *PIPlanInput) string { var b strings.Builder // ── 标题 ── b.WriteString("# PI 规划架构蓝图与任务清单\n\n") // ── 1. PI 愿景 ── b.WriteString("## 1. PI 愿景\n\n") b.WriteString(strings.TrimSpace(p.PIVision)) b.WriteString("\n\n") // ── 2. 业务特性清单 (Features) ── b.WriteString("## 2. 业务特性清单 (Features)\n\n") for _, f := range p.Features { b.WriteString(fmt.Sprintf("### %s — %s\n\n", f.FeatureID, f.Title)) b.WriteString(fmt.Sprintf("**业务价值假设**: %s\n\n", f.BenefitHypothesis)) b.WriteString("**验收标准 (AC)**:\n\n") for j, ac := range f.AcceptanceCriteria { b.WriteString(fmt.Sprintf("- [ ] AC-%d: %s\n", j+1, ac)) } b.WriteString("\n") } // ── 3. 技术赋能特性 (Enablers / 架构跑道) ── b.WriteString("## 3. 技术赋能特性 (Enablers / 架构跑道)\n\n") if len(p.Enablers) == 0 { b.WriteString("_无技术赋能特性。_\n\n") } else { b.WriteString("| Enabler ID | 名称 | 架构意图 |\n") b.WriteString("|------------|------|----------|\n") for _, e := range p.Enablers { b.WriteString(fmt.Sprintf("| %s | %s | %s |\n", e.EnablerID, e.Title, e.ArchitecturalPurpose)) } b.WriteString("\n") } // ── 4. 非功能性需求 (NFRs) ── b.WriteString("## 4. 非功能性需求 (NFRs)\n\n") b.WriteString(fmt.Sprintf("- **性能**: %s\n", p.NFRs.Performance)) b.WriteString(fmt.Sprintf("- **安全与合规**: %s\n", p.NFRs.Security)) b.WriteString("\n") // ── 5. 依赖关系图 ── b.WriteString("## 5. 依赖关系\n\n") if len(p.Dependencies) == 0 { b.WriteString("_无跨任务依赖。_\n\n") } else { b.WriteString("| 前置任务 (Source) | 后续任务 (Target) | 依赖原因 |\n") b.WriteString("|-------------------|-------------------|----------|\n") for _, d := range p.Dependencies { reason := d.Reason if reason == "" { reason = "—" } b.WriteString(fmt.Sprintf("| %s | %s | %s |\n", d.SourceID, d.TargetID, reason)) } b.WriteString("\n") } // ── 6. 建议执行顺序 ── b.WriteString("## 6. 建议执行顺序\n\n") order := computeExecutionOrder(p) for i, id := range order { b.WriteString(fmt.Sprintf("%d. %s\n", i+1, id)) } b.WriteString("\n") // ── 7. 质量门禁检查清单 ── b.WriteString("## 7. 质量门禁检查清单\n\n") b.WriteString("### 业务验收测试用例\n\n") for _, f := range p.Features { for j, ac := range f.AcceptanceCriteria { b.WriteString(fmt.Sprintf("- [ ] [%s] AC-%d: %s\n", f.FeatureID, j+1, ac)) } } b.WriteString("\n### 非功能性验证\n\n") b.WriteString(fmt.Sprintf("- [ ] 性能压测: %s\n", p.NFRs.Performance)) b.WriteString(fmt.Sprintf("- [ ] 安全扫描: %s\n", p.NFRs.Security)) b.WriteString("\n") return b.String() } // computeExecutionOrder 根据依赖关系计算拓扑排序的执行顺序。 // 先排 Enabler,再排 Feature;无依赖的排在前面。 func computeExecutionOrder(p *PIPlanInput) []string { // 收集所有 ID allIDs := make([]string, 0, len(p.Enablers)+len(p.Features)) idSet := make(map[string]bool) for _, e := range p.Enablers { allIDs = append(allIDs, e.EnablerID) idSet[e.EnablerID] = true } for _, f := range p.Features { allIDs = append(allIDs, f.FeatureID) idSet[f.FeatureID] = true } // 构建入度表和邻接表 inDegree := make(map[string]int) adj := make(map[string][]string) for _, id := range allIDs { inDegree[id] = 0 } for _, d := range p.Dependencies { if !idSet[d.SourceID] || !idSet[d.TargetID] { continue } adj[d.SourceID] = append(adj[d.SourceID], d.TargetID) inDegree[d.TargetID]++ } // Kahn 拓扑排序 queue := make([]string, 0) // 先加入度为 0 的 Enabler,再加入度为 0 的 Feature,保持稳定顺序 for _, e := range p.Enablers { if inDegree[e.EnablerID] == 0 { queue = append(queue, e.EnablerID) } } for _, f := range p.Features { if inDegree[f.FeatureID] == 0 { queue = append(queue, f.FeatureID) } } var result []string for len(queue) > 0 { curr := queue[0] queue = queue[1:] result = append(result, curr) for _, next := range adj[curr] { inDegree[next]-- if inDegree[next] == 0 { queue = append(queue, next) } } } // 如果存在环,将未排序的节点追加到末尾并标记 if len(result) < len(allIDs) { for _, id := range allIDs { if inDegree[id] > 0 { result = append(result, id+" ⚠️(循环依赖)") } } } return result }