Files
LaodingBot/tools/piplan/piplan.go

310 lines
8.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}