merge: candidate fix-quality-heading-eleven into local_dev

This commit is contained in:
weichen
2026-09-24 02:26:38 +00:00
3 changed files with 101 additions and 12 deletions
+45 -12
View File
@@ -702,23 +702,56 @@ func extractHeadings(md string) []string {
return headings
}
func cleanHeadingTitle(title string) string {
// 去除前缀序号如 "一、", "二、", "1.", "1.1", "(一)" 等
t := strings.TrimSpace(title)
runes := []rune(t)
// 去掉中文序号如 "一、", "二、", "三、", "四、", "五、", "六、", "七、", "八、", "九、", "十、"
chineseNums := []string{"一、", "二、", "三、", "四、", "五、", "六、", "七、", "八、", "九、", "十、"}
for _, cn := range chineseNums {
if strings.HasPrefix(t, cn) {
t = strings.TrimPrefix(t, cn)
break
}
func isChineseNumeralRune(r rune) bool {
switch r {
case '零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '百', '千', '两':
return true
default:
return false
}
// 去掉数字序号如 "1. ", "1、"
}
// stripChineseOrdinalPrefix 去掉「一、」「十一、」「二十三、」等中文序号前缀。
// 旧实现只枚举到「十、」,长文档 LLM 常用「十一、签署」会导致与模板「签署」对不上。
func stripChineseOrdinalPrefix(t string) string {
runes := []rune(t)
if len(runes) == 0 || !isChineseNumeralRune(runes[0]) {
return t
}
i := 0
for i < len(runes) && isChineseNumeralRune(runes[i]) {
i++
}
if i >= len(runes) {
return t
}
switch runes[i] {
case '、', '.', '.':
i++
default:
return t
}
for i < len(runes) && (runes[i] == ' ' || runes[i] == '\t') {
i++
}
return string(runes[i:])
}
func stripArabicOrdinalPrefix(t string) string {
runes := []rune(t)
// 去掉数字序号如 "1. ", "1、", "1.1 "
for len(runes) > 0 && ((runes[0] >= '0' && runes[0] <= '9') || runes[0] == '.' || runes[0] == '、' || runes[0] == ' ') {
t = strings.TrimSpace(string(runes[1:]))
runes = []rune(t)
}
return t
}
func cleanHeadingTitle(title string) string {
// 去除前缀序号如 "一、", "十一、", "1.", "1.1" 等
t := strings.TrimSpace(title)
t = stripChineseOrdinalPrefix(t)
t = stripArabicOrdinalPrefix(t)
// 去除可能括号后缀
if idx := strings.Index(t, "("); idx > 0 {
t = strings.TrimSpace(t[:idx])
@@ -0,0 +1,4 @@
package squad
// CleanHeadingTitleForTest 暴露 cleanHeadingTitle,供同模块白盒用例验证序号清洗。
var CleanHeadingTitleForTest = cleanHeadingTitle
+52
View File
@@ -480,3 +480,55 @@ func TestQualityGate_ActionPlanStatus_DoesNotTriggerRepair(t *testing.T) {
}
}
}
// 现场 nightly TestManager:LLM 写「## 十一、签署 / ## 十二、说明」,旧清洗只剥到「十、」会误报缺章。
func TestCleanHeadingTitle_MultiDigitChineseOrdinal(t *testing.T) {
cases := []struct {
in, want string
}{
{"一、客户与项目画像", "客户与项目画像"},
{"十、附录", "附录"},
{"十一、签署", "签署"},
{"十二、说明", "说明"},
{"二十三、风险评估", "风险评估"},
{"1. 结论", "结论"},
{"签署", "签署"},
{"十一、签署(终稿)", "签署"},
}
for _, c := range cases {
got := squad.CleanHeadingTitleForTest(c.in)
if got != c.want {
t.Errorf("cleanHeadingTitle(%q) = %q, want %q", c.in, got, c.want)
}
}
}
// 现场 nightly TestManager:LLM 写「## 十一、签署 / ## 十二、说明」,旧清洗只剥到「十、」会误报缺章。
func TestQualityGate_Validate_ChineseOrdinalElevenTwelve(t *testing.T) {
tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine()
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "ws_tm"),
TaskKey: "test_manager",
StageTitle: "系统验证方案及计划",
ProjectName: "医疗监护仪",
TemplateContent: "# 系统验证方案及计划\n\n" +
"## 结论\n\n## 用例与验证规程\n\n## 签署\n\n## 说明\n",
})
if err != nil {
t.Fatalf("PrepareWorkspace: %v", err)
}
gate := squad.NewQualityGate()
body := "# 系统验证方案及计划\n\n" +
"## 一、结论\n本方案覆盖核心验证路径,结论可交付。\n\n" +
"## 二、用例与验证规程\n| TC-01 | 验证心率测量精度 | 追溯 REQ-01 | 判定准则误差 <= ±2bpm | 通过 |\n\n" +
"## 十一、签署\n产品负责人签字确认。\n\n" +
"## 十二、说明\n本说明补充验证边界与假设。\n"
rep, err := gate.Validate(ws.WorkDir, "test_manager", body)
if err != nil {
t.Fatalf("Validate: %v", err)
}
if !rep.Passed {
t.Fatalf("expected eleven/twelve ordinal headings to match template, violations=%v", rep.Violations)
}
}