package validator import ( "context" "errors" "fmt" "strings" ) // CompositionMode controls how CompositeValidator aggregates child // verdicts. Two modes are provided as a baseline; more exotic // compositions (weighted voting, AND/OR trees) belong in the consuming // platform layer to avoid bloating core reference implementations. // // CompositionMode 决定 CompositeValidator 如何聚合子 verdict. 两种 // 模式作为基础; 加权投票 / AND-OR 树等更复杂的组合属于消费层职责, // 避免臃肿 core 参考实现. type CompositionMode int const ( // ModeAllMustApprove runs every child and approves only if every // child approves. Severity is the worst among children; Score is // the minimum. Provides a complete breakdown for audit -- errors // from any child are collected via errors.Join so errors.Is still // identifies class. // // ModeAllMustApprove 运行所有子 Validator, 全部 approve 才整体 // approve. Severity 取最严格, Score 取最小. 提供完整 breakdown // 供审计 -- 任一子的 error 经 errors.Join 合并, errors.Is 仍可 // 识别 class. ModeAllMustApprove CompositionMode = iota // ModeWaterfall runs children in order and short-circuits on the // first reject or error. Intended for cheap-first ordering (e.g. // RuleValidator before LLMValidator) so the expensive validator is // only reached when cheaper ones pass. // // ModeWaterfall 按顺序运行子 Validator, 遇第一个 reject 或 error // 即停. 面向便宜优先 (如 RuleValidator 先于 LLMValidator), 便宜的 // 通过才到达昂贵的. ModeWaterfall ) // CompositeValidator combines multiple Validators into one. Used to // layer cheap rules before LLM calls (Waterfall), or to require every // Validator's approval (AllMustApprove). Realises CLAUDE.md principle // 10 "overlay not replace" -- platforms can stack any number of // Validators in arbitrary combinations. // // CompositeValidator 组合多个 Validator. 用于便宜规则先于 LLM 调用 // (Waterfall), 或要求所有 Validator 都过 (AllMustApprove). 实现 CLAUDE.md // 原则 10 "叠加而非替换" -- 平台可以按任意组合叠加任意数量 Validator. type CompositeValidator struct { name string policyVersion string mode CompositionMode children []Validator } // NewCompositeValidator builds a CompositeValidator. name identifies // this composite for audit; policyVersion is the composition tag // (distinct from child Validators' own versions). mode selects the // aggregation strategy. children may be empty (trivial approve). // // NewCompositeValidator 构造 CompositeValidator. name 标识本 composite // 供审计; policyVersion 是组合标签 (与子 Validator 自身版本独立). mode // 选聚合策略. children 可为空 (trivial approve). func NewCompositeValidator(name, policyVersion string, mode CompositionMode, children ...Validator) *CompositeValidator { return &CompositeValidator{ name: name, policyVersion: policyVersion, mode: mode, children: children, } } // Name returns the composite's identifier. // // Name 返回 composite 标识符. func (c *CompositeValidator) Name() string { return c.name } // Validate dispatches to the configured CompositionMode aggregator. // An empty children list approves with Score=1.0 (vacuously satisfied). // Unknown mode returns a fail-closed Block verdict plus an error. // // Validate 按配置的 CompositionMode 聚合器分发. children 为空时直接 // approve (Score=1.0, vacuous 满足). 未知 mode 返回 fail-closed Block // verdict 附带 error. func (c *CompositeValidator) Validate(ctx context.Context, diff DiffInput) (Verdict, error) { if err := ctx.Err(); err != nil { return c.blockVerdict("context cancelled", nil), err } if len(c.children) == 0 { return c.emptyApproveVerdict(), nil } switch c.mode { case ModeWaterfall: return c.validateWaterfall(ctx, diff) case ModeAllMustApprove: return c.validateAll(ctx, diff) default: msg := fmt.Sprintf("unknown composition mode %d", c.mode) return c.blockVerdict(msg, nil), fmt.Errorf("validator: %s", msg) } } // validateAll runs every child, aggregates verdicts, joins all errors // via errors.Join. A child returning non-nil error is treated as Block // in the aggregation (fail-closed) but does not stop subsequent children // from running -- callers of ModeAllMustApprove want a complete audit. // // validateAll 运行所有子 Validator, 聚合 verdict, 用 errors.Join 合并 // 所有 error. 子返回非 nil error 时在聚合中按 Block 处置 (fail-closed), // 但不阻止后续子运行 -- ModeAllMustApprove 调用方要完整审计. func (c *CompositeValidator) validateAll(ctx context.Context, diff DiffInput) (Verdict, error) { details := make(map[string]any, len(c.children)) var errs []error approved := true worst := SeverityWarn minScore := 1.0 reasons := make([]string, 0, len(c.children)) for _, child := range c.children { sub, err := child.Validate(ctx, diff) details[child.Name()] = childDetail(sub, err) if err != nil { errs = append(errs, fmt.Errorf("%s: %w", child.Name(), err)) approved = false worst = SeverityBlock minScore = 0 reasons = append(reasons, fmt.Sprintf("%s: %v", child.Name(), err)) continue } if !sub.Approved { approved = false if sub.Severity == SeverityBlock { worst = SeverityBlock } if sub.Reason != "" { reasons = append(reasons, fmt.Sprintf("%s: %s", child.Name(), sub.Reason)) } } if sub.Score < minScore { minScore = sub.Score } } return Verdict{ Approved: approved, Score: minScore, Reason: strings.Join(reasons, "; "), Severity: worst, Details: details, ValidatorName: c.name, PolicyVersion: c.policyVersion, }, errors.Join(errs...) } // validateWaterfall runs children in order, short-circuiting on the // first reject or error. Returned Verdict carries the rejecting child's // reason and severity; Details includes every child actually run (the // rejecting one included, those after it skipped). // // validateWaterfall 按顺序运行子 Validator, 遇第一个 reject 或 error // 即停. 返回的 Verdict 承载 reject 子的 reason / severity; Details 含 // 所有实际运行的子 (含 reject 的; 其后跳过). func (c *CompositeValidator) validateWaterfall(ctx context.Context, diff DiffInput) (Verdict, error) { details := make(map[string]any, len(c.children)) minScore := 1.0 for _, child := range c.children { sub, err := child.Validate(ctx, diff) details[child.Name()] = childDetail(sub, err) if err != nil { return Verdict{ Approved: false, Score: 0, Reason: fmt.Sprintf("%s: %v", child.Name(), err), Severity: SeverityBlock, Details: details, ValidatorName: c.name, PolicyVersion: c.policyVersion, }, fmt.Errorf("%s: %w", child.Name(), err) } if !sub.Approved { return Verdict{ Approved: false, Score: sub.Score, Reason: fmt.Sprintf("%s: %s", child.Name(), sub.Reason), Severity: sub.Severity, Details: details, ValidatorName: c.name, PolicyVersion: c.policyVersion, }, nil } if sub.Score < minScore { minScore = sub.Score } } return Verdict{ Approved: true, Score: minScore, Severity: SeverityWarn, Details: details, ValidatorName: c.name, PolicyVersion: c.policyVersion, }, nil } // childDetail packs a child's Verdict into a map suitable for a parent // Details breakdown. err != nil is attached so aggregated reports // retain the error source even after errors.Join collapses the chain. // // childDetail 把子 Verdict 打包为 map 供父 Details breakdown 使用. err // != nil 一并附上, 让聚合报告在 errors.Join 合并后仍保留 error 来源. func childDetail(v Verdict, err error) map[string]any { d := map[string]any{ "approved": v.Approved, "score": v.Score, "severity": string(v.Severity), "reason": v.Reason, } if v.Details != nil { d["details"] = v.Details } if err != nil { d["error"] = err.Error() } return d } // blockVerdict constructs a fail-closed Verdict for unhappy paths // (context cancelled, unknown mode). // // blockVerdict 构造 fail-closed Verdict 用于非 happy 路径 (context // 取消 / 未知 mode). func (c *CompositeValidator) blockVerdict(reason string, details map[string]any) Verdict { return Verdict{ Approved: false, Severity: SeverityBlock, Reason: reason, Details: details, ValidatorName: c.name, PolicyVersion: c.policyVersion, } } // emptyApproveVerdict is the trivial approve for an empty children // list -- a composite with nothing to run vacuously passes. // // emptyApproveVerdict 是空 children 的 trivial approve -- 没子要跑的 // composite 按空集合 vacuous 通过. func (c *CompositeValidator) emptyApproveVerdict() Verdict { return Verdict{ Approved: true, Score: 1.0, Severity: SeverityWarn, ValidatorName: c.name, PolicyVersion: c.policyVersion, } }