Files
Agent-System/internal/traceimpact/policy.go
T
weichenandCursor 33ee59641f feat(trace): add the project trace ledger and bounded impact walk
Keep item revisions and edge decisions as records, and refuse to treat a missing index as no impact.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 15:35:07 +00:00

115 lines
3.3 KiB
Go

package traceimpact
import "fmt"
// Actor is the person trying to confirm or reject a candidate edge.
type Actor struct {
UserID int64
Creator bool
AuthorOf map[string]bool
}
// CanDecide reports whether actor may confirm or reject a model candidate
// on the downstream document. There is no separate trace_reviewer role.
func CanDecide(actor Actor, downstreamDocID string, downstreamAuthorID int64) error {
if downstreamDocID == "" {
return fmt.Errorf("traceimpact: missing downstream document")
}
if downstreamAuthorID > 0 {
if actor.UserID == downstreamAuthorID {
return nil
}
return fmt.Errorf("traceimpact: only the downstream author may decide this candidate")
}
if actor.Creator {
return nil
}
return fmt.Errorf("traceimpact: no downstream author; only the project creator may decide")
}
// DutySeat is one document's author, reviewer, and approver.
type DutySeat struct {
TaskKey string
AuthorID int64
ReviewerID int64
ApproverID int64
}
// MayRequestReopen allows the project creator or a duty holder on a selected document.
func MayRequestReopen(userID int64, creator bool, selected []string, seats []DutySeat) bool {
if creator {
return true
}
wanted := map[string]bool{}
for _, key := range selected {
wanted[key] = true
}
for _, seat := range seats {
if !wanted[seat.TaskKey] {
continue
}
if userID > 0 && (userID == seat.AuthorID || userID == seat.ReviewerID || userID == seat.ApproverID) {
return true
}
}
return false
}
// Suggest returns the three allowed next steps. They do not change state by themselves.
func Suggest(coverage string, hasImpact bool) []Suggestion {
switch coverage {
case ImpactFailed, ImpactIndexPending, ImpactPartial:
return []Suggestion{{
Action: "wait_index",
Reason: "索引未完成或分析失败,不能判断为无影响",
}}
}
out := []Suggestion{{
Action: ClearNoChange,
Reason: "确认这次修改不需要改其他文档",
}}
if hasImpact || coverage == ImpactPossible {
out = append(out,
Suggestion{Action: "l1_revise", Reason: "只修订受影响文档,阶段保持完成"},
Suggestion{Action: "l2_reopen", Reason: "需要重跑编制或补充文档时再打开阶段"},
)
}
return out
}
// SummaryForCoverage is the sentence shown when there is no confirmed hit.
func SummaryForCoverage(coverage string) string {
switch coverage {
case ImpactNoKnown:
return "索引已覆盖,未发现已知关联"
case ImpactIndexPending:
return "分析尚未完成,不能判断为无影响"
case ImpactPartial:
return "结果不完整,不能判断为无影响"
case ImpactFailed:
return "分析失败,不能判断为无影响"
case ImpactPossible:
return "存在未确认的可能影响"
case ImpactDependencyUnchanged:
return "已有关联,相关片段未变化"
default:
return "存在已确认影响"
}
}
// RedactItems drops statements from documents the viewer cannot see.
// Hidden items are counted and not returned, so titles cannot leak.
func RedactItems(items []Item, visible func(docID string) bool) (out []Item, hidden int) {
for _, item := range items {
if item.SourceDocID != "" && (visible == nil || !visible(item.SourceDocID)) {
hidden++
continue
}
out = append(out, item)
}
if out == nil {
out = []Item{}
}
return out, hidden
}