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>
205 lines
5.3 KiB
Go
205 lines
5.3 KiB
Go
package squad
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// RDevStageOverlay 按参考项目 StageOverlay 规则拼接本阶段纪律,并按角色过滤。
|
|
// 模板仍管章节结构;这里只提供写作纪律与粒度下限。
|
|
func RDevStageOverlay(taskKey, role, tier string) string {
|
|
meta, ok := LookupRDevTask(taskKey)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
stageKey := strings.TrimPrefix(taskKey, "rd_")
|
|
root := findOverlayRoot()
|
|
if root == "" {
|
|
return ""
|
|
}
|
|
names := rdevOverlayNames(stageKey, meta.Phase, string(meta.Kind), tier)
|
|
var parts []string
|
|
for _, name := range names {
|
|
content := readOverlayFile(root, name)
|
|
if content == "" {
|
|
continue
|
|
}
|
|
if filtered := filterOverlayByRole(content, role); filtered != "" {
|
|
parts = append(parts, filtered)
|
|
}
|
|
}
|
|
return strings.TrimSpace(strings.Join(parts, "\n\n"))
|
|
}
|
|
|
|
func rdevOverlayNames(stageKey, phase, artifactKind, tier string) []string {
|
|
seen := map[string]bool{}
|
|
var names []string
|
|
add := func(name string) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" || seen[name] {
|
|
return
|
|
}
|
|
seen[name] = true
|
|
names = append(names, name)
|
|
}
|
|
for _, path := range []string{
|
|
"overlays/_shared/doc-structure.md",
|
|
"overlays/_shared/self-contained-handoff.md",
|
|
"overlays/_shared/acceptance-rubric.md",
|
|
"overlays/_shared/granularity.md",
|
|
"overlays/_shared/anti-truncation.md",
|
|
} {
|
|
add(path)
|
|
}
|
|
add("rdev")
|
|
if artifactKind != "" && artifactKind != "document" {
|
|
add("kind_" + artifactKind)
|
|
}
|
|
add("rdev_ph" + strings.TrimSpace(phase))
|
|
if strings.HasPrefix(stageKey, "sw_") {
|
|
add("rdev_software")
|
|
add("kind_software")
|
|
}
|
|
if ko := rdevInferredKind(stageKey); ko != "" {
|
|
add(ko)
|
|
}
|
|
add(stageKey)
|
|
switch strings.ToLower(strings.TrimSpace(tier)) {
|
|
case "high", "medium":
|
|
add("complexity_" + strings.ToLower(strings.TrimSpace(tier)))
|
|
}
|
|
switch stageKey {
|
|
case "user_needs", "system_reqs":
|
|
add("overlays/_shared/ur-rs-expansion.md")
|
|
}
|
|
if stageKey == "design_vv" || stageKey == "validation_plan" || stageKey == "tech_req" || (strings.HasPrefix(stageKey, "sw_") && strings.Contains(stageKey, "test")) {
|
|
add("overlays/_shared/rs-tsys-expansion.md")
|
|
}
|
|
return names
|
|
}
|
|
|
|
func rdevInferredKind(stageKey string) string {
|
|
switch {
|
|
case strings.HasPrefix(stageKey, "risk_"), stageKey == "hazard_analysis":
|
|
return "kind_risk"
|
|
case strings.Contains(stageKey, "validation"), strings.Contains(stageKey, "verification"), stageKey == "design_vv", stageKey == "user_test", stageKey == "clinical", stageKey == "biocompatibility", stageKey == "emc_safety", stageKey == "cleaning", stageKey == "animal_study":
|
|
return "kind_verification"
|
|
case strings.HasPrefix(stageKey, "design_"), strings.HasPrefix(stageKey, "system_"), stageKey == "tech_req", stageKey == "user_needs", stageKey == "process_flow", stageKey == "proposal", stageKey == "planning_review", stageKey == "req_review", stageKey == "input_phase_review", stageKey == "output_review":
|
|
return "kind_design_record"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func findOverlayRoot() string {
|
|
wd, _ := os.Getwd()
|
|
candidates := []string{wd}
|
|
dir := wd
|
|
for i := 0; i < 8; i++ {
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
break
|
|
}
|
|
dir = parent
|
|
candidates = append(candidates, dir)
|
|
}
|
|
if env := strings.TrimSpace(os.Getenv("APP_ROOT")); env != "" {
|
|
candidates = append(candidates, env)
|
|
}
|
|
candidates = append(candidates, "/app")
|
|
for _, root := range candidates {
|
|
if root == "" {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "configs", "overlays", "rdev.md")); err == nil {
|
|
return root
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func readOverlayFile(root, name string) string {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" || strings.Contains(name, "..") {
|
|
return ""
|
|
}
|
|
var rel string
|
|
if strings.Contains(name, "/") {
|
|
rel = filepath.Join("configs", filepath.FromSlash(name))
|
|
} else {
|
|
rel = filepath.Join("configs", "overlays", name+".md")
|
|
}
|
|
b, err := os.ReadFile(filepath.Join(root, rel))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
|
|
func filterOverlayByRole(text, role string) string {
|
|
text = strings.TrimSpace(text)
|
|
if text == "" {
|
|
return ""
|
|
}
|
|
role = strings.ToLower(strings.TrimSpace(role))
|
|
if role == "rdev_leader" {
|
|
role = "leader"
|
|
}
|
|
lines := strings.Split(text, "\n")
|
|
hasRole := false
|
|
for _, line := range lines {
|
|
if _, ok := parseRoleHeading(line); ok {
|
|
hasRole = true
|
|
break
|
|
}
|
|
}
|
|
if !hasRole {
|
|
return text
|
|
}
|
|
var out []string
|
|
include := true
|
|
for _, line := range lines {
|
|
if r, ok := parseRoleHeading(line); ok {
|
|
include = role != "" && r == role
|
|
if include {
|
|
out = append(out, line)
|
|
}
|
|
continue
|
|
}
|
|
if include {
|
|
out = append(out, line)
|
|
}
|
|
}
|
|
return strings.TrimSpace(strings.Join(out, "\n"))
|
|
}
|
|
|
|
func parseRoleHeading(line string) (string, bool) {
|
|
trim := strings.TrimSpace(line)
|
|
if !strings.HasPrefix(trim, "### ") {
|
|
return "", false
|
|
}
|
|
rest := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(trim, "### ")))
|
|
if !strings.HasPrefix(rest, "role:") {
|
|
return "", false
|
|
}
|
|
role := strings.TrimSpace(rest[len("role:"):])
|
|
if role == "" || strings.ContainsAny(role, " \t") {
|
|
return "", false
|
|
}
|
|
return role, true
|
|
}
|
|
|
|
func rdevTierFromWorkspace(workDir string) string {
|
|
b, err := os.ReadFile(filepath.Join(workDir, "blackboard.json"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var bb BlackboardState
|
|
if json.Unmarshal(b, &bb) != nil {
|
|
return ""
|
|
}
|
|
return rdevComplexityTier(bb.Fields)
|
|
}
|