Each rdev step now gets a role-filtered overlay so writing discipline stays out of the template. User needs, system requirements, review minutes, and upload indexes fail the quality gate when IDs, decisions, or attachment indexes are incomplete. Co-authored-by: Cursor <cursoragent@cursor.com>
97 lines
2.7 KiB
Go
97 lines
2.7 KiB
Go
package squad
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// ValidateQualityGateV2 按 quality_gate_key 执行结构化门禁。
|
|
func ValidateQualityGateV2(gateKey, wsDir string) []string {
|
|
switch gateKey {
|
|
case "quality.system_reqs.v2":
|
|
return validateRequirementsJSON(wsDir)
|
|
case "quality.test_manager.v2":
|
|
return validateTestCasesJSON(wsDir)
|
|
case "quality.rd_user_needs.v1":
|
|
body, _ := os.ReadFile(filepath.Join(wsDir, "output.md"))
|
|
return rdevRequirementIssues(wsDir, string(body), "UR")
|
|
case "quality.rd_system_reqs.v1":
|
|
body, _ := os.ReadFile(filepath.Join(wsDir, "output.md"))
|
|
return rdevRequirementIssues(wsDir, string(body), "RS")
|
|
case "quality.rd_review_minutes.v1":
|
|
body, _ := os.ReadFile(filepath.Join(wsDir, "output.md"))
|
|
return rdevReviewMinutesIssues(string(body))
|
|
case "quality.rd_upload_index.v1":
|
|
body, _ := os.ReadFile(filepath.Join(wsDir, "output.md"))
|
|
return rdevUploadIndexIssues(string(body))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func validateRequirementsJSON(wsDir string) []string {
|
|
path := filepath.Join(wsDir, "artifacts", "requirements.json")
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return []string{"requirements.json 为必需产物"}
|
|
}
|
|
var reqs []map[string]any
|
|
if err := json.Unmarshal(raw, &reqs); err != nil {
|
|
return []string{"requirements.json 格式无效"}
|
|
}
|
|
ids := map[string]bool{}
|
|
issues := make([]string, 0)
|
|
for _, r := range reqs {
|
|
id, _ := r["id"].(string)
|
|
if id == "" {
|
|
issues = append(issues, "需求缺少 id")
|
|
continue
|
|
}
|
|
if ids[id] {
|
|
issues = append(issues, "重复需求 id: "+id)
|
|
}
|
|
ids[id] = true
|
|
for _, field := range []string{"verification_method", "acceptance_criteria"} {
|
|
if v, _ := r[field].(string); strings.TrimSpace(v) == "" {
|
|
issues = append(issues, id+": 缺少 "+field)
|
|
}
|
|
}
|
|
}
|
|
return issues
|
|
}
|
|
|
|
func validateTestCasesJSON(wsDir string) []string {
|
|
path := filepath.Join(wsDir, "artifacts", "test_cases.json")
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return []string{"test_cases.json 为必需产物"}
|
|
}
|
|
var cases []map[string]any
|
|
if err := json.Unmarshal(raw, &cases); err != nil {
|
|
return []string{"test_cases.json 格式无效"}
|
|
}
|
|
reqPath := filepath.Join(wsDir, "artifacts", "requirements.json")
|
|
reqRaw, err := os.ReadFile(reqPath)
|
|
if err != nil {
|
|
return []string{"缺少上游 requirements.json 无法验证覆盖率"}
|
|
}
|
|
var reqs []map[string]any
|
|
_ = json.Unmarshal(reqRaw, &reqs)
|
|
covered := map[string]bool{}
|
|
for _, tc := range cases {
|
|
if rid, _ := tc["requirement_id"].(string); rid != "" {
|
|
covered[rid] = true
|
|
}
|
|
}
|
|
issues := make([]string, 0)
|
|
for _, r := range reqs {
|
|
id, _ := r["id"].(string)
|
|
if id != "" && !covered[id] {
|
|
issues = append(issues, "需求未覆盖: "+id)
|
|
}
|
|
}
|
|
return issues
|
|
}
|