package staging import ( "context" "fmt" "git.flytoex.net/yuanwei/flyto-agent/pkg/validator" ) // Engine wires a Store, two validator.Validator implementations // (tech + biz tiers), and a DependencyGuard into the mixed-control // state machine (product decision Y). It owns the two internal arcs // (pending_tech -> rejected_tech | pending_ml and // pending_ml -> rejected_ml | approved) by actively calling the // Validators. The terminal approved -> executed | failed arc is // driven externally: the platform layer calls MarkExecuted or // MarkFailed on the Store (or on Engine, which delegates) once the // WMS / production write resolves. // // Engine is stateless -- all persistence lives in Store. Safe for // concurrent use; the Store and Validators underneath carry the // concurrency burden. // // Engine 将 Store / 两个 validator.Validator (技术 + 业务层) / // DependencyGuard 组装为混合控制状态机 (产品决策 Y). 它以主动调 // Validator 的方式拥有内部两段 arc (pending_tech -> rejected_tech | // pending_ml 与 pending_ml -> rejected_ml | approved). 终端 // approved -> executed | failed arc 由外部驱动: 平台层在 WMS / 生产 // 写动作落地后调 Store 的 MarkExecuted 或 MarkFailed (或 Engine 同名 // 方法, Engine 仅转发). // // Engine 无状态 -- 持久化全在 Store. 并发安全由底层 Store 与 // Validator 承担. type Engine struct { store Store tech validator.Validator biz validator.Validator guard DependencyGuard } // NewEngine constructs an Engine. All four arguments are required; // passing nil for any of them panics (consistent with // validator.NewValidatedTool and reflector adapters: construction- // time fail-fast surfaces wiring errors at startup instead of at // the first request). // // NewEngine 构造 Engine. 四个参数均必填; 任一为 nil 直接 panic // (对齐 validator.NewValidatedTool 与 reflector adapter 的构造期 // fail-fast 模式, 把接线错误在启动时暴露, 而非推到首次请求). func NewEngine(store Store, tech, biz validator.Validator, guard DependencyGuard) *Engine { if store == nil { panic("staging.NewEngine: nil Store") } if tech == nil { panic("staging.NewEngine: nil tech Validator") } if biz == nil { panic("staging.NewEngine: nil biz Validator") } if guard == nil { panic("staging.NewEngine: nil DependencyGuard (pass AllowAlwaysGuard{} to opt out)") } return &Engine{store: store, tech: tech, biz: biz, guard: guard} } // Stage delegates to Store.Stage with a freshly constructed // pending_tech Record carrying the given Diff under sessionID. // // Stage 向 Store.Stage 转发, 用给定 Diff 和 sessionID 构造 // pending_tech Record. func (e *Engine) Stage(ctx context.Context, sessionID string, diff validator.DiffInput) (Record, error) { return e.store.Stage(ctx, NewDraft(sessionID, diff)) } // ValidateTech runs the tech-tier Validator against the Record's // Diff and drives pending_tech -> rejected_tech | pending_ml. // // Failure semantics: // // - validator.Validator returns a non-nil error. The Engine // treats this fail-closed: it synthesizes a Verdict // {Approved=false, Severity=Block, Reason=wrapped error, // ValidatorName=tech.Name()} and routes the Record to // rejected_tech. A broken Validator must not silently pass. // - DependencyGuard returns (false, _) or (_, non-nil error) on // the approved-path (i.e. the Validator said Approved=true). // Engine returns ErrDependencyDenied without persisting any // state change; caller retries later or surfaces to humans. // // The rejected-path (Validator said Approved=false) does NOT // consult the guard -- denying a rejection is not a meaningful // operation, and consulting the guard would let a misconfigured // guard block the rejection audit trail. // // ValidateTech 用技术层 Validator 审 Record.Diff, 推 pending_tech -> // rejected_tech | pending_ml. // // 失败语义: // // - validator.Validator 返回非 nil error. Engine 按 fail-closed // 处理: 合成 Verdict {Approved=false, Severity=Block, // Reason=包装后的错误, ValidatorName=tech.Name()} 并把 Record // 路由到 rejected_tech. 坏掉的 Validator 不能静默放行. // - DependencyGuard 在放行路径 (Validator 说 Approved=true) 返回 // (false, _) 或 (_, 非 nil error). Engine 返回 ErrDependencyDenied // 不持久化状态变更; 调用方可稍后重试或上报人工. // // 拒绝路径 (Validator 说 Approved=false) 不咨询 guard -- 对拒绝 // 再拒绝无意义, 且咨询 guard 会让一个配错的 guard 阻断拒绝审计链. func (e *Engine) ValidateTech(ctx context.Context, id string) (Record, error) { r, err := e.store.Get(ctx, id) if err != nil { return Record{}, err } if r.State != StatePendingTech { if r.State.IsFinal() { return r, ErrAlreadyFinal } return r, fmt.Errorf("%w: ValidateTech from %q", ErrIllegalTransition, r.State) } v, verr := e.tech.Validate(ctx, r.Diff) if verr != nil { v = validator.Verdict{ Approved: false, Severity: validator.SeverityBlock, Reason: fmt.Sprintf("tech validator error: %v", verr), ValidatorName: e.tech.Name(), } } if v.Approved { allow, gerr := e.guard.AllowTransition(ctx, r, StatePendingML) if gerr != nil || !allow { return r, ErrDependencyDenied } } return e.store.UpdateTechVerdict(ctx, id, v) } // ValidateBiz runs the biz-tier Validator against the Record's // Diff and drives pending_ml -> rejected_ml | approved with the // same fail-closed + guard semantics as ValidateTech. // // ValidateBiz 用业务层 Validator 审 Record.Diff, 推 pending_ml -> // rejected_ml | approved, fail-closed 与 guard 语义同 ValidateTech. func (e *Engine) ValidateBiz(ctx context.Context, id string) (Record, error) { r, err := e.store.Get(ctx, id) if err != nil { return Record{}, err } if r.State != StatePendingML { if r.State.IsFinal() { return r, ErrAlreadyFinal } return r, fmt.Errorf("%w: ValidateBiz from %q", ErrIllegalTransition, r.State) } v, verr := e.biz.Validate(ctx, r.Diff) if verr != nil { v = validator.Verdict{ Approved: false, Severity: validator.SeverityBlock, Reason: fmt.Sprintf("biz validator error: %v", verr), ValidatorName: e.biz.Name(), } } if v.Approved { allow, gerr := e.guard.AllowTransition(ctx, r, StateApproved) if gerr != nil || !allow { return r, ErrDependencyDenied } } return e.store.UpdateBizVerdict(ctx, id, v) } // MarkExecuted exposes the external-push arc: the platform layer // calls this after a successful production commit, attaching proof // (WMS receipt / idempotency key / transaction id) for audit. // Thin delegation to Store.MarkExecuted. // // MarkExecuted 暴露外部推入 arc: 平台层在生产 commit 成功后调用, // 附 proof (WMS 回单 / 幂等 key / 事务 id) 用于审计. 薄转发 // Store.MarkExecuted. func (e *Engine) MarkExecuted(ctx context.Context, id string, proof any) (Record, error) { return e.store.MarkExecuted(ctx, id, proof) } // MarkFailed exposes the external-push arc for failed production // commit, with reason for the audit trail. Thin delegation. // // MarkFailed 暴露生产 commit 失败的外部推入 arc, reason 用于审计. // 薄转发. func (e *Engine) MarkFailed(ctx context.Context, id string, reason string) (Record, error) { return e.store.MarkFailed(ctx, id, reason) }