Files
Agent-System/internal/docduty/resolve.go
T
weichenandCursor 9a19bcc12c fix(docduty): revoke inactive slots and explicit duty changes
Empty assigned heal now clears only exited or non-member reviewer/approver
holders and never blanks author. Explicit reconcile uses the pre-update
duty plus a project advisory lock so viewer demotion and transfers are
not left to generic heal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 22:29:53 +00:00

183 lines
4.9 KiB
Go

package docduty
import (
"fmt"
"strings"
)
const (
DutyNone = ""
DutyAuthor = "author"
DutyReviewer = "reviewer"
DutyApprover = "approver"
StatusActive = "active"
)
// Holder 某一文档职责的用户。
type Holder struct {
UserID int64
Name string
}
// Binding 一份受控文档应采用的编制/审核/批准职责。
type Binding struct {
Author Holder
Reviewer Holder
Approver Holder
}
// Member 项目成员快照(访问角色与文档职责分离)。
type Member struct {
UserID int64
Name string
Status string
ProjectRole string
DocumentDuty string
}
// RequestDuties 任务请求里客户端声称的职责;仅当该用户是在任成员时才可采用。
type RequestDuties struct {
AuthorID int64
AuthorName string
ReviewerID int64
ReviewerName string
ApproverID int64
ApproverName string
}
// NormalizeDocumentDuty 校验 document_duty。空串表示无文档职责。
func NormalizeDocumentDuty(raw string) (string, error) {
duty := strings.TrimSpace(strings.ToLower(raw))
switch duty {
case DutyNone, DutyAuthor, DutyReviewer, DutyApprover:
return duty, nil
default:
return "", fmt.Errorf("docduty: invalid document_duty %q", raw)
}
}
// ActiveMemberIDs 在任成员(含创建者)。
func ActiveMemberIDs(members []Member, creator Holder) map[int64]struct{} {
ids := make(map[int64]struct{}, len(members)+1)
if creator.UserID > 0 {
ids[creator.UserID] = struct{}{}
}
for _, m := range members {
if m.UserID > 0 && (m.Status == "" || m.Status == StatusActive) {
ids[m.UserID] = struct{}{}
}
}
return ids
}
func isActiveMember(m Member) bool {
return m.UserID > 0 && (m.Status == "" || m.Status == StatusActive)
}
func dutyHolder(members []Member, duty string) Holder {
var found Holder
for _, m := range members {
if !isActiveMember(m) || m.DocumentDuty != duty {
continue
}
found = Holder{UserID: m.UserID, Name: strings.TrimSpace(m.Name)}
}
return found
}
func pickMember(reqID int64, reqName string, memberIDs map[int64]struct{}, members []Member) Holder {
if reqID <= 0 {
return Holder{}
}
if _, ok := memberIDs[reqID]; !ok {
return Holder{}
}
name := strings.TrimSpace(reqName)
for _, m := range members {
if m.UserID == reqID && isActiveMember(m) && strings.TrimSpace(m.Name) != "" {
name = strings.TrimSpace(m.Name)
break
}
}
return Holder{UserID: reqID, Name: name}
}
// Resolve 以项目 document_duty 为权威。禁止回落到硬编码 2/3。
// author:指定编制人 → 创建者 → 请求 id(须为成员)。
// reviewer/approver:指定职责 → 请求 id(须为成员)→ 空。
func Resolve(members []Member, creator Holder, req RequestDuties) Binding {
memberIDs := ActiveMemberIDs(members, creator)
out := Binding{}
if h := dutyHolder(members, DutyAuthor); h.UserID > 0 {
out.Author = h
} else if creator.UserID > 0 {
out.Author = creator
} else {
out.Author = pickMember(req.AuthorID, req.AuthorName, memberIDs, members)
}
if h := dutyHolder(members, DutyReviewer); h.UserID > 0 {
out.Reviewer = h
} else {
out.Reviewer = pickMember(req.ReviewerID, req.ReviewerName, memberIDs, members)
}
if h := dutyHolder(members, DutyApprover); h.UserID > 0 {
out.Approver = h
} else {
out.Approver = pickMember(req.ApproverID, req.ApproverName, memberIDs, members)
}
return out
}
// DutyMissing 当前职责缺失或指向非成员(含历史硬编码占位 id)。
func DutyMissing(currentID int64, memberIDs map[int64]struct{}) bool {
if currentID <= 0 {
return true
}
_, ok := memberIDs[currentID]
return !ok
}
func healSlot(current, assigned Holder, memberIDs map[int64]struct{}, clearInactive bool) (Holder, bool) {
if assigned.UserID > 0 {
if current.UserID == assigned.UserID && current.Name == assigned.Name {
return current, false
}
return assigned, true
}
if current.UserID <= 0 {
return current, false
}
if _, ok := memberIDs[current.UserID]; ok {
return current, false
}
if !clearInactive {
return current, false
}
return Holder{}, true
}
// HealBinding 以项目 document_duty 为权威。assigned 非零则对齐(错 ID 或同 ID 旧名称)。
// assigned 为空时:审核/批准仅清空已退出或非成员的失效 holder;在任成员保留以兼容未初始化 duty 的旧项目。
// 编制槽位即使 assigned 为空也不清空,避免无意去掉 owner/author。
func HealBinding(current, assigned Binding, memberIDs map[int64]struct{}) (Binding, bool) {
out := current
changed := false
if next, ok := healSlot(current.Author, assigned.Author, memberIDs, false); ok {
out.Author = next
changed = true
}
if next, ok := healSlot(current.Reviewer, assigned.Reviewer, memberIDs, true); ok {
out.Reviewer = next
changed = true
}
if next, ok := healSlot(current.Approver, assigned.Approver, memberIDs, true); ok {
out.Approver = next
changed = true
}
return out, changed
}