fix:stalled 分類同步 agents#47——assignee 交付報告待確認改判 waiting-outside #49
@@ -237,6 +237,11 @@ PR 改帶 `author` 與 `reviewers` 欄位。`reason` 取值:
|
||||
- issue:`no-assignee`、`assignee-idle`、`no-commenter`、`waiting-outside`、`parent-tracking`
|
||||
- PR:`no-reviewer`、`author-idle`、`reviewer-idle`、`no-commenter`
|
||||
|
||||
`assignee-idle` 判定前會先檢查最後一則 assignee 留言是否為**交付報告**(agents #47):
|
||||
逐項查核/驗收結果(結構訊號)∧ 回報他件/請他人確認關閉(交棒訊號)同時命中 →
|
||||
改判 `waiting-outside`——已交付待發起人確認不是 assignee 停滯。認領/進度/預告式
|
||||
留言(無交棒對象)維持 `assignee-idle`,不製造「認領後棄置永不被催」的新死角。
|
||||
|
||||
### `teai milestones` — 跨倉庫里程碑總覽
|
||||
|
||||
```sh
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package workflow
|
||||
|
||||
import "sort"
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sortComments 依 created_at 字典序穩定排序(ISO8601 字串字典序與時間序一致),
|
||||
// 對應 gitea.py 各處的 comments.sort(key=created_at)。
|
||||
@@ -135,3 +139,65 @@ func LastCommentNotByAssignee(issue *Issue, comments []Comment) bool {
|
||||
func IssueNeedsWork(issue *Issue, comments []Comment, members []string) bool {
|
||||
return AssigneeOverlaps(issue, members) && LastCommentNotByAssignee(issue, comments)
|
||||
}
|
||||
|
||||
// ── 交付報告判定(agents #47,同步 gitea.py _is_delivery_report)──────────
|
||||
//
|
||||
// 最後一則 assignee 留言已把工作交棒給發起人/他人確認(逐項查核/驗收
|
||||
// 結果+回報他件或請確認關閉)時,停滯點不在 assignee:stalled 分類由
|
||||
// assignee-idle 改判 waiting-outside,管理者巡邏不再對已交付、待確認的
|
||||
// 處理者觸發自動催促。
|
||||
|
||||
var (
|
||||
// deliveryVerdictRe 對應 gitea.py _DELIVERY_VERDICT_RE:彙總 verdict 語。
|
||||
deliveryVerdictRe = regexp.MustCompile(`(全數|全部|\d+\s*項)通過|項全過|不通過|未通過`)
|
||||
// deliveryHandoffRe 對應 gitea.py _DELIVERY_HANDOFF_RE:交棒訊號。
|
||||
deliveryHandoffRe = regexp.MustCompile(`回報\s*#\d+|關閉\s*#\d+|請[^\n。]{0,40}(確認|關閉)|可由[^\n。]{0,40}關閉|待[^\n。]{0,20}確認`)
|
||||
// completionDefinitionRe 對應 _has_completion_definition:內文「完成定義」
|
||||
// 段落(ATX 標題或行首字樣)。
|
||||
completionDefinitionRe = regexp.MustCompile(`(?m)^#{1,6}\s*完成定義|^完成定義\s*[::]?`)
|
||||
// simpleDeliveryRe 內文有「完成定義」時的簡式交付語。
|
||||
simpleDeliveryRe = regexp.MustCompile(`已完成|已交付|已通過|已全數`)
|
||||
)
|
||||
|
||||
// hasCompletionDefinition 內文是否含「完成定義」段落。
|
||||
func hasCompletionDefinition(body string) bool {
|
||||
return completionDefinitionRe.MatchString(body)
|
||||
}
|
||||
|
||||
// IsDeliveryReport 判定最後一則 assignee 留言是否符合交付報告特徵
|
||||
// (agents #47;gitea.py _is_delivery_report 的同語義實作)。
|
||||
//
|
||||
// 交付報告=「結構訊號」∧「交棒訊號」同時命中:
|
||||
// - 結構訊號:逐項查核/驗收/測試結果、結論、或彙總 verdict 語;內文有
|
||||
// 「完成定義」段落時,簡式交付語(已完成/已交付)也算。
|
||||
// - 交棒訊號:回報/關閉他件(#N)、或請/可由/待他人確認關閉。
|
||||
//
|
||||
// 只命中結構訊號不算:認領/進度/預告式留言(「完成後逐項回報結果至此」)
|
||||
// 沒有交棒對象,仍是 assignee 自己該做——保持 assignee-idle,修正不製造
|
||||
// 「認領後棄置永不被催促」的新死角。
|
||||
func IsDeliveryReport(issueBody string, last *Comment) bool {
|
||||
if last == nil {
|
||||
return false
|
||||
}
|
||||
body := last.Body
|
||||
if body == "" {
|
||||
return false
|
||||
}
|
||||
structural := false
|
||||
for _, marker := range []string{"查核結果", "驗收結果", "測試結果", "逐項", "結論", "完成定義"} {
|
||||
if strings.Contains(body, marker) {
|
||||
structural = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !structural && deliveryVerdictRe.MatchString(body) {
|
||||
structural = true
|
||||
}
|
||||
if !structural && hasCompletionDefinition(issueBody) && simpleDeliveryRe.MatchString(body) {
|
||||
structural = true
|
||||
}
|
||||
if !structural {
|
||||
return false
|
||||
}
|
||||
return deliveryHandoffRe.MatchString(body)
|
||||
}
|
||||
|
||||
@@ -143,3 +143,39 @@ func TestFullNameFallbacks(t *testing.T) {
|
||||
t.Errorf("全缺 → 空,得到 %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsDeliveryReport(agents #47):交付報告=結構訊號 ∧ 交棒訊號。
|
||||
// 案例與 gitea.py _is_delivery_report 的離線驗證同集(bear#52 實例)。
|
||||
func TestIsDeliveryReport(t *testing.T) {
|
||||
body52 := "## 完成定義\n- 逐項回報查核結果至此 issue;\n- 全數通過 → 回報 #39 並由 #39 assignee 關閉"
|
||||
claim := "認領。我來執行四項驗收查核,完成後逐項回報結果至此。"
|
||||
report := "## 全階段驗收查核結果(#52)\n**結論:四項全數通過。** 逐項如下…\n四項全過,符合「完成定義」。回報 #39:全數驗收通過,可由 @ceo 關閉 #39 與里程碑。"
|
||||
reportFail := "## 查核結果\n結論:第 2 項不通過。已開立修復 issue #60,請 @ceo 確認分配。"
|
||||
cases := []struct {
|
||||
name string
|
||||
issueBody string
|
||||
comment string
|
||||
want bool
|
||||
}{
|
||||
{"交付報告+回報#39+@ceo 關閉", body52, report, true},
|
||||
{"失敗型交付(開修復 issue+請確認)", body52, reportFail, true},
|
||||
{"認領留言(逐項+回報 但無交棒)", body52, claim, false},
|
||||
{"純進度留言", body52, "進行中,已看完 P1,預計今晚交付。", false},
|
||||
{"已完成但無交棒", body52, "已完成,程式碼都在分支上。", false},
|
||||
{"無完成定義內文+逐項交棒", "一般 issue 內文", "逐項測試結果全數通過,請 @ceo 確認後關閉。", true},
|
||||
{"無完成定義+僅交棒(無結構)", "一般 issue 內文", "請 @ceo 確認後關閉。", false},
|
||||
{"空留言", body52, "", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
var last *Comment
|
||||
if tc.comment != "" {
|
||||
last = &Comment{Body: tc.comment}
|
||||
}
|
||||
if got := IsDeliveryReport(tc.issueBody, last); got != tc.want {
|
||||
t.Errorf("%s:IsDeliveryReport = %v,want %v", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
if IsDeliveryReport(body52, nil) {
|
||||
t.Error("nil 最後留言應為 false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +526,13 @@ func considerIssue(src Source, org, repo string, issue Issue, comments []Comment
|
||||
case len(assignees) == 0:
|
||||
reason = "no-assignee"
|
||||
case lastCommentBy != "" && containsString(assignees, lastCommentBy):
|
||||
reason = "assignee-idle"
|
||||
if IsDeliveryReport(issue.Body, last) {
|
||||
// 交付報告待確認(agents #47):assignee 已交付並交棒(回報他件/
|
||||
// 請發起人確認關閉),停滯點在等待外部確認,不是 assignee 停滯。
|
||||
reason = "waiting-outside"
|
||||
} else {
|
||||
reason = "assignee-idle"
|
||||
}
|
||||
case lastCommentBy == "":
|
||||
reason = "no-commenter"
|
||||
}
|
||||
|
||||
@@ -595,3 +595,39 @@ func TestRound1(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanStalledDeliveryReport(agents #47):assignee 最後留言為交付報告
|
||||
// (結構訊號+交棒訊號)→ waiting-outside,不是 assignee-idle;
|
||||
// 同型認領留言(無交棒)維持 assignee-idle——修正不製造新死角。
|
||||
func TestScanStalledDeliveryReport(t *testing.T) {
|
||||
now := at(0)
|
||||
defBody := "## 完成定義\n- 逐項回報查核結果至此 issue;\n- 全數通過 → 回報 #39 並由 #39 assignee 關閉"
|
||||
src := &fakeSource{
|
||||
orgs: []string{"alterminal"},
|
||||
repos: map[string][]Repo{"alterminal": repoWith("bear", true)},
|
||||
issues: map[string]map[int64]Issue{
|
||||
"alterminal/bear": {
|
||||
5: {Number: 5, Title: "驗收", State: "open", Body: defBody, CreatedAt: iso(-600), UpdatedAt: iso(-300), Assignees: []User{{UserName: "queena"}}, Repository: &Repository{FullName: "alterminal/bear"}},
|
||||
6: {Number: 6, Title: "認領中", State: "open", Body: defBody, CreatedAt: iso(-600), UpdatedAt: iso(-300), Assignees: []User{{UserName: "dan"}}, Repository: &Repository{FullName: "alterminal/bear"}},
|
||||
},
|
||||
},
|
||||
comments: map[string][]Comment{
|
||||
"alterminal/bear/5": {{User: &User{UserName: "queena"}, CreatedAt: iso(-300), Body: "## 查核結果\n結論:四項全數通過。回報 #39:可由 @ceo 關閉。"}},
|
||||
"alterminal/bear/6": {{User: &User{UserName: "dan"}, CreatedAt: iso(-300), Body: "認領。完成後逐項回報結果至此。"}},
|
||||
},
|
||||
}
|
||||
got, err := ScanStalled(src, "admin", 4.0, now)
|
||||
if err != nil {
|
||||
t.Fatalf("ScanStalled: %v", err)
|
||||
}
|
||||
byNumber := map[int64]Item{}
|
||||
for _, it := range got {
|
||||
byNumber[it.Number] = it
|
||||
}
|
||||
if it, ok := byNumber[5]; !ok || it.Reason != "waiting-outside" {
|
||||
t.Errorf("issue 5(交付報告)reason 應為 waiting-outside,got %+v", it)
|
||||
}
|
||||
if it, ok := byNumber[6]; !ok || it.Reason != "assignee-idle" {
|
||||
t.Errorf("issue 6(認領留言)reason 應為 assignee-idle,got %+v", it)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user