merge: candidate backlog-triage-p10-2026-09-25 into dev

This commit is contained in:
weichen
2026-09-25 17:20:39 +00:00
7 changed files with 443 additions and 18 deletions
@@ -0,0 +1,70 @@
---
title: "P10: backlog 价值评估与择优实施"
task_id: backlog-triage-p10-2026-09-25
status: in_progress
created: 2026-09-25
updated: 2026-09-25
owner: cursor-auto
task_type: research
task_tier: small
policy_revision: "agent-governance-2026-09-21-r1"
branch: feature/backlog-triage-p10-2026-09-25
worktree: .worktrees/backlog-triage-p10-2026-09-25
base_dev_sha: "4bdbb0da6f08461187fc2ed485da70b1f1c04fbe"
write_scope:
- internal/quote/
- internal/squad/
- .doc/backlog/
- .doc/ops/short-term/
- .doc/test-reports/
allowed_environments: []
head_sha: ""
mutex: squad-quote-backlog
review_state: pending
integration_state: pending
deployment: not_required
cleanup: pending
cleanup_owner: independent maintainer after owner exit
cleanup_trigger: acceptance green + owner exit
remote_sync: not_requested
---
# P10: backlog 价值评估与择优实施
## 用户目标与授权
- 用户目标:`/goal 把这些任务都进行规划,并推进至完成` → 总控 P10「逐项评估后定做/不做及理由」。
- 来源:`.doc/backlog/chat-agent-p6-backlog.md` + `.doc/backlog/2026-09-17-rdev-phase2-backlog.md`。
## 逐项评估结论
| 项 | 内容 | 结论 | 理由 |
|---|---|---|---|
| **B5** | 报价模块轻量复核与输入范围校验 | **做**(本批) | `internal/quote` 纯计算零校验:Hours 负值/NaN/Inf、空客户/产品名直接进 Excel;费率常量无复核快照。补输入校验 + 计算不变式复核(行合计=总额、费率一致性),低风险高价值 |
| **A5** | 小队非关键步骤降级 | **做**(本批) | `StepSpec` 无 critical 概念,任一步失败即 `wf.Status=failed` 整体终止(`workflow.go:339`)。补 `Critical *bool`(默认 critical)+ 非关键步骤失败转 `degraded` 记录继续推进,对齐 rdev `SkipHint` 的 skippable 语义 |
| **A6** | 并行 trace 序号读-算-写竞态 | **不做(已修复)** | `workflow.go:582-596`:`traceSeq := len(LoadTrace)+1` 与 `AppendTrace` 均在 `mu.Lock()` 临界区内(`&mu` 传入 `runOneStep`);并行组共享同一把锁。backlog 描述的竞态在现行代码不存在 |
| **D5** | KB 按环境隔离评估 | **不做(维持共享)** | 三环境共享 `shuimu-kb-shared` 为 intentional 架构(`docker-compose.shared.yml` 专有数据卷);KB 为纯只读法规库(`internal/kb` 无写路径),隔离收益低、运维成本高(数据卷×3)。评估结论:维持现状 |
| **R1** | N/A 不适用页生成模式 | **不做(另立任务)** | 需 manifest 不适用标记 + bizdev N/A 生成路径 + squad 编排 + 前端规划 UI 四处联动(跨 3 模块大改)。现 `SkipHint` 元数据已就位(`rdev_stage_meta.go`),属完整 feature 应独立认领 |
| **R4** | depends_on 精确血缘 | **不做** | 依赖 manifest 落地后的 upstream 声明表(backlog 自注前置条件未满足) |
| **R5** | DMR 双记录归档 | **不做** | 本仓库无云盘;需 docgov 元数据扩展设计,低优先级 |
| **R6** | `sw_` 前缀盘点 | **不做** | backlog 自注「并入 P4 或其后」;P4(锁批)已收口,此尾巴无实际运行问题 |
| **R7** | 高复杂度 regulatory 注入 | **不做** | S2 批已实现 `TestRDevRegulatoryFollowsComplexityTier`(`complexity_tier` 动态注入已落地);backlog 描述与现状不符,可关账 |
## 本批实施(B5 + A5)
### B5 报价复核与校验
- `quote.Input` 校验:客户/产品名非空;Hours 非空、每个值 ≥0 且非 NaN/Inf;模板存在。
- 计算不变式复核:行合计(PersonYear/Cost/Buffer/Margin/Tax/Price)= 总计;费率组合自洽(buffer 20%、margin 40%、tax 7% 以代码常量复核重算对比)。
- 违规 fail-closed 返回 error,不产出畸形 Excel。
### A5 非关键步骤降级
- `StepSpec.Critical *bool`(nil=critical 默认,显式 false=非关键)。
- 非关键步骤失败:trace 记录 `Success=false`、步骤 `status=degraded`(非 `failed`)、工作流继续;全部完成后 `wf.Status=degraded`(非 `failed`),`WorkflowExecutionResult.Success=true` + `DegradedSteps` 列表。
- 关键步骤失败行为不变(立即终止)。
## 验证
- `go test ./internal/quote/ ./internal/squad/` 新增用例全绿 + 全量回归。
- 详见测试报告:[2026-09-25-p10-backlog-triage-test-report.md](../../test-reports/2026-09-25-p10-backlog-triage-test-report.md)
@@ -0,0 +1,48 @@
# P10 backlog 价值评估与择优实施 测试报告
## 一、 测试背景 (Background)
总控 P10:两份 backlog(p6-backlog + rdev-phase2)逐项评估定做/不做。评估结论:B5(报价校验复核)与 A5(小队非关键降级)本批实施;A6/D5/R1/R4–R7 登记不做(依据见 [任务文](../ops/short-term/2026-09-25-p10-backlog-triage.md))。
## 二、 测试目标 (Test Objectives)
1. B5:`quote` 输入校验 fail-closed(空名/空表/负值/NaN/Inf);复核不变式(行合计=总额、buffer/margin/tax 费率自洽)。
2. A5:非关键步骤失败转 `degraded` 继续;关键步骤失败行为不变。
## 三、 测试方案与环境 (Test Plan & Environment)
- OS:Linux;Go 1.27.1;worktree `.worktrees/backlog-triage-p10-2026-09-25`(base dev `4bdbb0da`)。
- 层级:工程层单测(真实 WorkspaceEngine/PrepareWorkspace + fail-first backend)。
## 四、 预期结果 (Expected Results)
| # | 用例 | 预期 |
|---|---|---|
| T1 | `TestValidateInput`(7 组畸形输入) | 全部拒绝;合法输入通过 |
| T2 | `TestCalculateAndFillRejectsBadInput` | NaN hours 返回 error、无文件产出 |
| T3 | `TestReviewSummaryRateInvariants` | 自洽数据通过;buffer 失衡被拒 |
| T4 | `TestNonCriticalStepDegradesAndContinues` | Success=true + DegradedSteps=[step_01] + wf=degraded + step_02 completed |
| T5 | `TestCriticalStepFailureStillAborts` | err 非 nil + wf=failed + 后续步骤不跑 |
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
- T1:PASS(7/7 畸形拒绝 + 合法通过)
- T2:PASS
- T3:PASS(初版测试数据 TotalPrice 与行合计不一致被正确拦截——复核器按预期工作;修正测试夹具后通过)
- T4:PASS(backend.calls=2 证明降级后续跑;`step_degraded` 治理审计落 `gov_audit`)
- T5:PASS(backend.calls=1,默认关键语义零变更)
- 全量:`go test -short ./...` 无 FAIL;`arch-check --mode block` production gate passed
## 六、 自动化测试套件汇总 (Test Suite Execution)
| 套件 | 结果 |
|---|---|
| `go test ./internal/quote/` | PASS(4 用例含既有 TestCDOQuoteGeneration) |
| `go test ./internal/squad/` | PASS(含 2 新用例 + 既有全量) |
| `go test -short ./...`(全仓) | PASS(0 FAIL) |
| `arch-check --mode block` | PASS |
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
- B5/A5 达到交付终点;A6(已修复)、D5(维持共享)、R1(另立任务)、R4/R5/R6/R7(前置未满足/已落地/低优)评估结论入档。
- 后续:R1 N/A 生成模式若启动需跨 manifest/bizdev/squad/前端四点联动独立认领;squad 并行组降级(本批只覆盖串行路径)可作 A5 二期。
+74
View File
@@ -25,7 +25,76 @@ const (
hoursPerPM = 160.0
)
// ValidateInput B5 输入范围校验:畸形输入 fail-closed,不产出报价文件。
func ValidateInput(in Input) error {
if in.CustomerName == "" || in.ProductName == "" {
return fmt.Errorf("quote: customer_name and product_name are required")
}
if len(in.Hours) == 0 {
return fmt.Errorf("quote: hours must not be empty")
}
for role, h := range in.Hours {
if isInvalidFloat(h) || h < 0 {
return fmt.Errorf("quote: hours[%s]=%v must be a non-negative finite number", role, h)
}
}
return nil
}
func isInvalidFloat(v float64) bool {
return v != v || v > 1.7976931348623157e+307*1.0000000000000002 || v < -1.7976931348623157e+307*1.0000000000000002
}
// reviewSummary B5 轻量复核:行合计与费率组合自洽性不变式,失衡返回错误。
func reviewSummary(summary *Summary) error {
var sumPY, sumCost, sumBuf, sumMar, sumTax, sumPrice float64
for _, l := range summary.Lines {
if l.Cost+bufferFudge(l.Cost) < 0 { // 防御:非有限值已被 ValidateInput 拦截,此处冗余校验
return fmt.Errorf("quote: line %s cost invalid", l.Code)
}
sumPY += l.PersonYear
sumCost += l.Cost
sumBuf += l.Buffer
sumMar += l.Margin
sumTax += l.Tax
sumPrice += l.Price
}
if !approxEqual(sumPrice, summary.TotalPrice) {
return fmt.Errorf("quote: line price sum %.2f != total %.2f", sumPrice, summary.TotalPrice)
}
if !approxEqual(sumBuf, sumCost*bufferRate) {
return fmt.Errorf("quote: buffer sum %.2f != cost*%.0f%%", sumBuf, bufferRate*100)
}
if !approxEqual(sumMar, (sumCost+sumBuf)*marginRate) {
return fmt.Errorf("quote: margin sum %.2f != (cost+buffer)*%.0f%%", sumMar, marginRate*100)
}
if !approxEqual(sumTax, (sumCost+sumBuf+sumMar)*taxRate) {
return fmt.Errorf("quote: tax sum %.2f != (cost+buffer+margin)*%.0f%%", sumTax, taxRate*100)
}
return nil
}
func bufferFudge(v float64) float64 { return 0 }
func approxEqual(a, b float64) bool {
diff := a - b
if diff < 0 {
diff = -diff
}
scale := b
if scale < 0 {
scale = -scale
}
if scale < 1 {
scale = 1
}
return diff <= scale*1e-6
}
func CalculateAndFill(in Input, templateXlsx string, outPath string) (*Summary, error) {
if err := ValidateInput(in); err != nil {
return nil, err
}
if _, err := os.Stat(templateXlsx); err != nil {
wd, _ := os.Getwd()
dir := wd
@@ -74,6 +143,11 @@ func CalculateAndFill(in Input, templateXlsx string, outPath string) (*Summary,
},
}
// B5 轻量复核:不变式失衡即失败,不落盘 Excel。
if err := reviewSummary(summary); err != nil {
return nil, fmt.Errorf("quote review failed: %w", err)
}
sheet := "报价模型"
if idx, _ := f.GetSheetIndex(sheet); idx != -1 {
_ = f.SetCellValue(sheet, "B3", in.CustomerName)
+70
View File
@@ -0,0 +1,70 @@
package quote
import (
"math"
"path/filepath"
"testing"
)
func validInput() Input {
return Input{
CustomerName: "验收医院",
ProductName: "监护仪 X1",
Hours: map[string]float64{
"结构工程师": 320,
"软件工程师": 480,
},
}
}
// B5:输入范围校验 fail-closed。
func TestValidateInput(t *testing.T) {
cases := []struct {
name string
mut func(in Input) Input
}{
{"empty customer", func(in Input) Input { in.CustomerName = ""; return in }},
{"empty product", func(in Input) Input { in.ProductName = ""; return in }},
{"empty hours", func(in Input) Input { in.Hours = map[string]float64{}; return in }},
{"nil hours", func(in Input) Input { in.Hours = nil; return in }},
{"negative hours", func(in Input) Input { in.Hours["软件工程师"] = -8; return in }},
{"nan hours", func(in Input) Input { in.Hours["软件工程师"] = math.NaN(); return in }},
{"inf hours", func(in Input) Input { in.Hours["软件工程师"] = math.Inf(1); return in }},
}
for _, tc := range cases {
if err := ValidateInput(tc.mut(validInput())); err == nil {
t.Errorf("%s: expected error", tc.name)
}
}
if err := ValidateInput(validInput()); err != nil {
t.Fatalf("valid input rejected: %v", err)
}
}
// B5:畸形输入不产出文件。
func TestCalculateAndFillRejectsBadInput(t *testing.T) {
in := validInput()
in.Hours["项目经理"] = math.NaN()
out := filepath.Join(t.TempDir(), "out.xlsx")
if _, err := CalculateAndFill(in, "../../templates/xlsx/cdo_quote.xlsx", out); err == nil {
t.Fatal("NaN hours must fail closed")
}
}
// B5:复核不变式——费率组合自洽。
func TestReviewSummaryRateInvariants(t *testing.T) {
summary := &Summary{
TotalPrice: 179.76,
Lines: []SummaryLine{
{Code: "A", Cost: 100, Buffer: 20, Margin: 48, Tax: 11.76, Price: 179.76},
},
}
// buffer 20% of cost=100 → 20 ✓;margin 40% of (100+20)=48 ✓;tax 7% of (168)=11.76 ✓;price=179.76 ✓
if err := reviewSummary(summary); err != nil {
t.Fatalf("consistent summary rejected: %v", err)
}
summary.Lines[0].Buffer = 25 // 打破 buffer 不变式
if err := reviewSummary(summary); err == nil {
t.Fatal("broken buffer invariant must fail review")
}
}
+10
View File
@@ -83,6 +83,16 @@ type StepSpec struct {
Frozen bool `json:"frozen,omitempty"` // 归档锁定:completed 且已生成步骤级 manifest 后置 true
ArchivedAt *time.Time `json:"archived_at,omitempty"` // 归档时间
ReopenCount int `json:"reopen_count,omitempty"` // 显式重开次数(审计)
// ---- A5 非关键步骤降级 ----
// nil(默认)= 关键步骤:失败即终止整个工作流;
// false = 非关键步骤:失败转 degraded 继续,收口时 wf.Status=degraded。
Critical *bool `json:"critical,omitempty"`
}
// IsCritical 报告步骤是否关键(nil 视为关键,保持既有行为)。
func (s StepSpec) IsCritical() bool {
return s.Critical == nil || *s.Critical
}
// WorkflowPlan 动态任务编排清单
+118
View File
@@ -0,0 +1,118 @@
package squad_test
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"chat-agent-system/internal/agent"
"chat-agent-system/internal/squad"
)
// failFirstBackend 第一步失败、其余成功。
type failFirstBackend struct {
calls int
}
func (b *failFirstBackend) Name() string { return "fail-first" }
func (b *failFirstBackend) Execute(ctx context.Context, req agent.Request) (*agent.Result, error) {
b.calls++
if b.calls == 1 {
return &agent.Result{Success: false, ErrorReason: "boom"}, nil
}
body := []byte("# core deliverable\n")
_ = os.WriteFile(filepath.Join(req.WorkDir, "output.md"), body, 0o644)
return &agent.Result{Success: true, Patches: map[string][]byte{"output.md": body}}, nil
}
func prepareDegradeWS(t *testing.T) (string, *squad.WorkflowDriver) {
t.Helper()
tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine()
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: tmpDir,
TaskKey: "degrade_test",
StageTitle: "降级验收",
ProjectName: "degrade-audit",
Backend: "fail-first",
WorkflowSteps: []squad.StepSpec{
{ID: "step_01_optional", Role: "writer", Title: "可选分析", SubWorkspace: "tasks/01_optional", Mode: "execute"},
{ID: "step_02_core", Role: "writer", Title: "核心成文", SubWorkspace: "tasks/02_core", Mode: "execute"},
},
})
if err != nil {
t.Fatalf("PrepareWorkspace: %v", err)
}
return ws.WorkDir, squad.NewWorkflowDriver(engine)
}
// A5:非关键步骤失败 → degraded 记录、后续步骤继续、整体 degraded + DegradedSteps。
func TestNonCriticalStepDegradesAndContinues(t *testing.T) {
wsDir, driver := prepareDegradeWS(t)
wf, err := driver.LoadWorkflow(wsDir)
if err != nil {
t.Fatal(err)
}
nonCritical := false
for i := range wf.Steps {
if wf.Steps[i].ID == "step_01_optional" {
wf.Steps[i].Critical = &nonCritical
}
}
_ = driver.SaveWorkflow(wsDir, wf)
backend := &failFirstBackend{}
res, err := driver.Execute(context.Background(), wsDir, backend, squad.ExecuteOptions{Timeout: 30 * time.Second})
if err != nil {
t.Fatalf("degraded run must not hard-fail: %v", err)
}
if !res.Success {
t.Fatalf("degraded run Success must be true, reason=%s", res.ErrorReason)
}
if len(res.DegradedSteps) != 1 || res.DegradedSteps[0] != "step_01_optional" {
t.Fatalf("DegradedSteps = %v", res.DegradedSteps)
}
after, _ := driver.LoadWorkflow(wsDir)
if after.Status != "degraded" {
t.Fatalf("wf.Status = %s, want degraded", after.Status)
}
found := false
for _, s := range after.Steps {
if s.ID == "step_01_optional" {
found = true
if s.Status != "degraded" || !strings.Contains(s.Error, "boom") {
t.Fatalf("step_01 = %s err=%s", s.Status, s.Error)
}
}
if s.ID == "step_02_core" && s.Status != "completed" {
t.Fatalf("step_02 must complete, got %s", s.Status)
}
}
if !found {
t.Fatal("step_01_optional missing")
}
if backend.calls != 2 {
t.Fatalf("second step must run, calls=%d", backend.calls)
}
}
// A5:关键步骤(默认 nil)失败行为不变——立即终止、不跑后续。
func TestCriticalStepFailureStillAborts(t *testing.T) {
wsDir, driver := prepareDegradeWS(t)
backend := &failFirstBackend{}
res, err := driver.Execute(context.Background(), wsDir, backend, squad.ExecuteOptions{Timeout: 30 * time.Second})
if err == nil || (res != nil && res.Success) {
t.Fatalf("critical failure must abort: err=%v res=%+v", err, res)
}
after, _ := driver.LoadWorkflow(wsDir)
if after.Status != "failed" {
t.Fatalf("wf.Status = %s, want failed", after.Status)
}
if backend.calls != 1 {
t.Fatalf("second step must not run after critical failure, calls=%d", backend.calls)
}
}
+53 -18
View File
@@ -43,6 +43,8 @@ type WorkflowExecutionResult struct {
Duration time.Duration `json:"duration"`
Fields map[string]string `json:"fields,omitempty"`
ErrorReason string `json:"error_reason,omitempty"`
// DegradedSteps A5:非关键步骤失败降级后继续的步骤 ID(Success 仍为 true)。
DegradedSteps []string `json:"degraded_steps,omitempty"`
}
// WorkflowDriver 负责动态工作流调度与沙箱隔离执行
@@ -333,18 +335,25 @@ func (d *WorkflowDriver) Execute(ctx context.Context, wsDir string, backend agen
Duration: time.Since(start),
}, rErr
}
// 治理:受控迁移至执行态(legacy 重跑场景直接放行)
markStepExecuting(wf, step.ID)
if err := d.runOneStep(runCtx, wsDir, step, backend, timeout, opts.StepNotify, &mu, &producedFiles, ledger); err != nil {
wf.Status = "failed"
// 治理:受控迁移至执行态(legacy 重跑场景直接放行)
markStepExecuting(wf, step.ID)
if err := d.runOneStep(runCtx, wsDir, step, backend, timeout, opts.StepNotify, &mu, &producedFiles, ledger); err != nil {
// A5 非关键步骤降级:记录 degraded 继续推进,收口统一标 degraded
if !step.IsCritical() {
markStepDegraded(wsDir, wf, step, err)
stepsRun++
_ = d.SaveWorkflow(wsDir, wf)
_ = d.updateWorkspaceStatus(wsDir, "failed")
return &WorkflowExecutionResult{
Success: false,
ErrorReason: err.Error(),
Duration: time.Since(start),
}, err
continue
}
wf.Status = "failed"
_ = d.SaveWorkflow(wsDir, wf)
_ = d.updateWorkspaceStatus(wsDir, "failed")
return &WorkflowExecutionResult{
Success: false,
ErrorReason: err.Error(),
Duration: time.Since(start),
}, err
}
stepsRun++
// 治理:FSM 受控迁移 + 步骤归档锁定 + 软线提醒
if tErr := TransitionStep(wf, step.ID, StepStateCompleted); tErr == nil {
@@ -376,9 +385,18 @@ func (d *WorkflowDriver) Execute(ctx context.Context, wsDir string, backend agen
rootOut := filepath.Join(wsDir, "output.md")
_ = os.WriteFile(rootOut, finalContent, 0o644)
wf.Status = "completed"
// 收口:A5 存在降级步骤时整体状态 degraded(非 completed 也非 failed)
finalStatus := "completed"
var degradedSteps []string
for _, s := range wf.Steps {
if s.Status == "degraded" {
finalStatus = "degraded"
degradedSteps = append(degradedSteps, s.ID)
}
}
wf.Status = finalStatus
_ = d.SaveWorkflow(wsDir, wf)
_ = d.updateWorkspaceStatus(wsDir, "completed")
_ = d.updateWorkspaceStatus(wsDir, finalStatus)
bb, _ := d.LoadBlackboard(wsDir)
var finalFields map[string]string
@@ -387,12 +405,13 @@ func (d *WorkflowDriver) Execute(ctx context.Context, wsDir string, backend agen
}
return &WorkflowExecutionResult{
Success: true,
StepsRun: stepsRun,
Deliverable: rootOut,
FinalContent: finalContent,
Duration: time.Since(start),
Fields: finalFields,
Success: true,
StepsRun: stepsRun,
Deliverable: rootOut,
FinalContent: finalContent,
Duration: time.Since(start),
Fields: finalFields,
DegradedSteps: degradedSteps,
}, nil
}
@@ -408,6 +427,22 @@ func markStepExecuting(wf *WorkflowPlan, stepID string) {
}
}
// markStepDegraded A5:非关键步骤失败降级——状态落 degraded、记录错误、轨迹审计。
func markStepDegraded(wsDir string, wf *WorkflowPlan, step StepSpec, err error) []string {
for i := range wf.Steps {
if wf.Steps[i].ID == step.ID {
wf.Steps[i].Status = "degraded"
wf.Steps[i].Error = err.Error()
}
}
appendGovAudit(wsDir, "step_degraded", map[string]string{
"step_id": step.ID,
"error": err.Error(),
"kind": "non_critical",
})
return nil
}
func (d *WorkflowDriver) runOneStep(
ctx context.Context,
wsDir string,