fix(docgov): keep squad AutoRepair/Success semantics; force repair only on controlled docs
小队修复预算只认 AutoRepair,Success 只表示执行完成;bizdev 在解析出 docRec 后强制开启修复,投影与阶段停链仍以 QualityReport 为准。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,7 +30,7 @@ severity: high
|
||||
|
||||
## 整改(同分支 `fix/document-revision-sse-idempotency`)
|
||||
|
||||
- A:有 `TaskKey` 的受控小队任务服务端强制 `DefaultMaxRepairRounds`;handlers 产品契约 `AutoRepair: true`
|
||||
- A:小队 `repairBudget` 只认 `AutoRepair`;bizdev 在解析出 `docID`+`docRec` 后强制开启;受控 HTTP `task_run_handlers` 仍设 `true`。小队 `Success` 仅表示执行完成,投影/阶段停链看 `QualityReport`
|
||||
- B:prompt 去原词;豁免仅限同一自检/规则行且同时具备 meta marker + 否定紧邻或引号引用(禁止 48 字窗口遇「无」即放过)
|
||||
- C:`TerminalStatus` 优先 quality;Materialize 仅 `Passed`;FE `quality_failed` 独立失败态且不同步激活
|
||||
- D:现场 L204 / repair budget / TerminalStatus / FE 映射单测
|
||||
|
||||
@@ -39,6 +39,7 @@ mutex: docgov
|
||||
9. [x] E 质量失败在 SetDocumentWorkDir/Materialize 前短路(无正式投影副作用)
|
||||
10. [x] 豁免收窄为同行 meta+否定紧邻/引号;handlers 阶段请求字段边界单测锁定
|
||||
11. [x] ExecuteStage 质量失败停链:不吸收事实、不追加 PriorDoc
|
||||
12. [x] 小队 AutoRepair/Success 语义恢复;受控修复仅在 bizdev docRec 与 HTTP 边界强制
|
||||
|
||||
## 验收标准
|
||||
|
||||
|
||||
@@ -9,10 +9,23 @@ import (
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/config"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/docrevision"
|
||||
"chat-agent-system/internal/squad"
|
||||
)
|
||||
|
||||
func TestAutoRepairForControlledSquad(t *testing.T) {
|
||||
if autoRepairForControlledSquad(false, "", nil) {
|
||||
t.Fatal("uncontrolled must respect AutoRepair=false")
|
||||
}
|
||||
if !autoRepairForControlledSquad(true, "", nil) {
|
||||
t.Fatal("uncontrolled AutoRepair=true must stay true")
|
||||
}
|
||||
if !autoRepairForControlledSquad(false, "doc_proj1_intake", &docgov.DocumentRecord{DocID: "doc_proj1_intake"}) {
|
||||
t.Fatal("resolved controlled doc must force AutoRepair")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualityBlocksControlledProjection(t *testing.T) {
|
||||
if qualityBlocksControlledProjection(nil) {
|
||||
t.Fatal("nil report must not block")
|
||||
|
||||
@@ -698,7 +698,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
|
||||
Timeout: req.Timeout,
|
||||
WorkDir: req.WorkDir,
|
||||
CustomSteps: req.CustomSteps,
|
||||
AutoRepair: req.AutoRepair,
|
||||
AutoRepair: autoRepairForControlledSquad(req.AutoRepair, docID, docRec),
|
||||
KnowledgeHits: hits,
|
||||
PriorDocs: priorDocs,
|
||||
UserContext: userCtxStr,
|
||||
@@ -735,6 +735,14 @@ func qualityBlocksControlledProjection(report *squad.QualityReport) bool {
|
||||
return report != nil && !report.Passed
|
||||
}
|
||||
|
||||
// autoRepairForControlledSquad 已解析出正式受控文档后强制开启修复;非受控调用仍尊重请求值。
|
||||
func autoRepairForControlledSquad(reqAutoRepair bool, docID string, docRec *docgov.DocumentRecord) bool {
|
||||
if strings.TrimSpace(docID) != "" && docRec != nil {
|
||||
return true
|
||||
}
|
||||
return reqAutoRepair
|
||||
}
|
||||
|
||||
func qualityFailedErrorReason(report *squad.QualityReport) string {
|
||||
if report == nil {
|
||||
return "质量门禁未通过"
|
||||
|
||||
@@ -20,7 +20,7 @@ func (s *Service) executeStageTask(ctx context.Context, req TaskRunRequest) (*Ta
|
||||
}
|
||||
|
||||
// classifyStageTaskHalt 阶段链在 ExecuteTask err=nil 后的中止判定。
|
||||
// 质量失败(含仅 Success=false + QualityReport.Passed=false)必须停链:
|
||||
// 质量失败以 QualityReport 为准(小队 Success 仍可表示执行完成),必须停链:
|
||||
// 不吸收 ExtractedData、不追加 PriorDoc、不宣称阶段成功。
|
||||
func classifyStageTaskHalt(res *TaskRunResult, tKey string) *stageTaskHalt {
|
||||
if res == nil {
|
||||
|
||||
@@ -82,8 +82,9 @@ func TestExecuteStage_QualityFailed_StopsChainWithoutAbsorbingFactsOrPriorDoc(t
|
||||
sentinelKey: "poison_from_quality_fail",
|
||||
"device_type": "should_not_land_on_blackboard",
|
||||
},
|
||||
WorkDir: filepath.Join(stageWork, "research"),
|
||||
Success: false,
|
||||
WorkDir: filepath.Join(stageWork, "research"),
|
||||
// 小队执行完成可为 true;阶段停链看 QualityReport,不依赖 Success=false
|
||||
Success: true,
|
||||
ErrorReason: "质量门禁未通过:省略/敷衍写法",
|
||||
QualityReport: &squad.QualityReport{
|
||||
Passed: false,
|
||||
|
||||
@@ -2,24 +2,20 @@ package squad
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRepairBudgetForSquadTask_ProductContract(t *testing.T) {
|
||||
// 受控文档小队任务:即使客户端省略 auto_repair=false,也必须给修复预算
|
||||
func TestRepairBudgetForSquadTask_RespectsAutoRepairOnly(t *testing.T) {
|
||||
got := repairBudgetForSquadTask(SquadRunRequest{TaskKey: "intake", AutoRepair: false})
|
||||
if got != DefaultMaxRepairRounds {
|
||||
t.Fatalf("intake AutoRepair=false budget=%d want %d", got, DefaultMaxRepairRounds)
|
||||
if got != 0 {
|
||||
t.Fatalf("TaskKey alone must not enable repair: budget=%d", got)
|
||||
}
|
||||
if DefaultMaxRepairRounds < 1 {
|
||||
t.Fatalf("DefaultMaxRepairRounds must be >=1, got %d", DefaultMaxRepairRounds)
|
||||
}
|
||||
|
||||
// 无 TaskKey 且未开 AutoRepair:保持 0(非受控路径)
|
||||
got = repairBudgetForSquadTask(SquadRunRequest{TaskKey: "", AutoRepair: false})
|
||||
if got != 0 {
|
||||
t.Fatalf("empty TaskKey without AutoRepair budget=%d want 0", got)
|
||||
}
|
||||
|
||||
got = repairBudgetForSquadTask(SquadRunRequest{TaskKey: "", AutoRepair: true})
|
||||
got = repairBudgetForSquadTask(SquadRunRequest{TaskKey: "intake", AutoRepair: true})
|
||||
if got != DefaultMaxRepairRounds {
|
||||
t.Fatalf("AutoRepair=true budget=%d want %d", got, DefaultMaxRepairRounds)
|
||||
}
|
||||
if DefaultMaxRepairRounds < 1 {
|
||||
t.Fatalf("DefaultMaxRepairRounds must be >=1, got %d", DefaultMaxRepairRounds)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-23
@@ -32,22 +32,22 @@ type Attachment struct {
|
||||
|
||||
// SquadRunRequest 运行请求
|
||||
type SquadRunRequest struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
Title string `json:"title,omitempty"` // 自由任务/非标任务的人类可读标题(缺省回落 TaskArtifactTitle)
|
||||
ProjectName string `json:"project_name"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
IntendedUse string `json:"intended_use"`
|
||||
TargetMarket string `json:"target_market"`
|
||||
Fields map[string]string `json:"fields"`
|
||||
Materials []MaterialItem `json:"materials"`
|
||||
Backend string `json:"backend"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
CustomSteps []StepSpec `json:"custom_steps"`
|
||||
AutoRepair bool `json:"auto_repair"`
|
||||
KnowledgeHits []KnowledgeHit `json:"knowledge_hits"`
|
||||
PriorDocs []PriorDocItem `json:"prior_docs"`
|
||||
TaskKey string `json:"task_key"`
|
||||
Title string `json:"title,omitempty"` // 自由任务/非标任务的人类可读标题(缺省回落 TaskArtifactTitle)
|
||||
ProjectName string `json:"project_name"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
IntendedUse string `json:"intended_use"`
|
||||
TargetMarket string `json:"target_market"`
|
||||
Fields map[string]string `json:"fields"`
|
||||
Materials []MaterialItem `json:"materials"`
|
||||
Backend string `json:"backend"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
CustomSteps []StepSpec `json:"custom_steps"`
|
||||
AutoRepair bool `json:"auto_repair"`
|
||||
KnowledgeHits []KnowledgeHit `json:"knowledge_hits"`
|
||||
PriorDocs []PriorDocItem `json:"prior_docs"`
|
||||
UserContext string `json:"user_context,omitempty"`
|
||||
MemoryBankFiles map[string]string `json:"memory_bank_files,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
@@ -295,7 +295,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
|
||||
if backendName == "stub" {
|
||||
finalBytes, _ = os.ReadFile(filepath.Join(workDir, "template.md"))
|
||||
_ = os.WriteFile(finalOutPath, finalBytes, 0o644)
|
||||
// stub 终稿以 template 为准:门禁须对齐最终交付物,避免对中间草稿误伤 Success/Materialize
|
||||
// stub 终稿以 template 为准:QualityReport 对齐最终交付物(Success 仍只表示执行完成)
|
||||
if reReport, reErr := s.gate.Validate(workDir, req.TaskKey, string(finalBytes)); reErr == nil {
|
||||
report = reReport
|
||||
manifest, _ = s.gate.GenerateManifest(workDir, req.TaskKey, *report)
|
||||
@@ -356,19 +356,16 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
|
||||
WorkDir: workDir,
|
||||
BackendUsed: backendName,
|
||||
Duration: time.Since(start),
|
||||
Success: execRes.Success && (report == nil || report.Passed),
|
||||
Success: execRes.Success,
|
||||
QualityReport: report,
|
||||
Manifest: manifest,
|
||||
AdvisorBrief: advisorBrief,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// repairBudgetForSquadTask 产品契约:有 TaskKey 的受控文档小队任务默认启用修复轮预算,
|
||||
// 不依赖客户端 auto_repair(浏览器可省略且默认为 false)。
|
||||
// repairBudgetForSquadTask 仅尊重请求方 AutoRepair,不因 TaskKey 非空而改写 CLI/桩/非受控语义。
|
||||
// 受控文档任务的修复预算由 bizdev/HTTP 在调用前强制 AutoRepair=true。
|
||||
func repairBudgetForSquadTask(req SquadRunRequest) int {
|
||||
if strings.TrimSpace(req.TaskKey) != "" {
|
||||
return DefaultMaxRepairRounds
|
||||
}
|
||||
if req.AutoRepair {
|
||||
return DefaultMaxRepairRounds
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user