package audit import ( "context" "strings" "sync" ) // DecisionNote 把本次代审的**判定理由**带出冻结的调用契约:代审门(capability/restrict) // 的 Reviewer 只回 (allowed, error),而前端小字要写清「为什么被拒」,因此理由经这条 // **按调用**(不是跨调用)的通道交给组合根,由它拼进小字。零值可用;全部方法对 nil 安全。 type DecisionNote struct { mu sync.Mutex reason string } // WithDecisionNote 在 ctx 上挂一个全新的按调用理由盒,供本次代审链路写入: // // ctx, note := audit.WithDecisionNote(ctx) // decision := gate.Admit(ctx, call.Name, call.Args) // 代审在内部写入理由 // if r := note.Reason(); r != "" { …拼进前端小字… } // // 每次工具调用挂一个新的盒子,因此并发的两次代审绝不互相覆盖理由。 func WithDecisionNote(ctx context.Context) (context.Context, *DecisionNote) { note := &DecisionNote{} return context.WithValue(ctx, decisionNoteKey{}, note), note } // decisionNoteKey 是理由盒的 context key。 type decisionNoteKey struct{} // DecisionNoteFrom 返回 ctx 上挂着的理由盒;未挂载时返回 nil(各方法对 nil 安全)。 func DecisionNoteFrom(ctx context.Context) *DecisionNote { if ctx == nil { return nil } note, _ := ctx.Value(decisionNoteKey{}).(*DecisionNote) return note } // SetReason 记录本次代审的判定理由。空白理由被忽略,且**绝不覆盖**已有的非空理由: // 一次调用只有一个判定结论,后到的写入不能把先到的事实改掉。 func (n *DecisionNote) SetReason(reason string) { if n == nil || strings.TrimSpace(reason) == "" { return } n.mu.Lock() defer n.mu.Unlock() if n.reason == "" { n.reason = reason } } // Reason 返回已记录的理由;未记录时返回 ""。 func (n *DecisionNote) Reason() string { if n == nil { return "" } n.mu.Lock() defer n.mu.Unlock() return n.reason }