package staging import "context" // DependencyGuard gates each transition on an orthogonal dependency // condition (product decision #3). IsLegal covers the static state // matrix; DependencyGuard covers caller-defined ordering rules that // sit outside the matrix -- e.g. "this Record must wait until its // upstream Record is executed", "this Record's decision references // an external approval ticket that has not cleared yet". // // core intentionally does not prescribe a dependency shape. Callers // encode whatever semantics their domain needs inside their // AllowTransition implementation, typically reading hints from // Record.Metadata. This mirrors the plug-in model used by // validator.Validator and reflector's adapters. // // Error distinguishes guard failure from a deliberate deny: // // - (true, nil) -> transition may proceed. // - (false, nil) -> guard consciously denies; engine treats // it as a signal to retry later or surface to a human. // - (_, non-nil) -> guard could not decide (backend failure); // engine MUST fail closed (treat as deny) so a broken guard // does not silently wave everything through. // // AllowTransition is invoked AFTER the Validator produces its Verdict // and BEFORE the Store persists the transition, so guards can base // decisions on the Verdict as well as the Record. // // DependencyGuard 给每个 transition 加一个正交依赖条件闸 // (产品决策 #3). IsLegal 覆盖静态状态矩阵; DependencyGuard 覆盖 // 调用方定义的矩阵外的顺序规则 -- 例如 "此 Record 必须等上游 // Record executed", "此 Record 的决策引用了一个尚未清的外部审批单". // // core 刻意不规定依赖形态. 调用方在自己的 AllowTransition 实现里编 // 入业务所需语义, 通常读取 Record.Metadata 的 hint. 与 validator.Validator // 和 reflector adapter 所用的 plug-in 模型一致. // // error 区分 guard 失败与主动拒: // // - (true, nil) -> transition 可进行. // - (false, nil) -> guard 主动拒; engine 视为 "稍后重试或 // 上报人工" 的信号. // - (_, non-nil) -> guard 无法决策 (后端故障); engine 必须 // fail-closed (按拒处理), 避免坏 guard 悄悄放行. // // AllowTransition 在 Validator 产出 Verdict 之后 / Store 持久化之前 // 被调用, guard 因此既能看到 Verdict 也能看到 Record. type DependencyGuard interface { // AllowTransition reports whether r may move to to. // // AllowTransition 判定 r 能否转移到 to. AllowTransition(ctx context.Context, r Record, to State) (allow bool, err error) } // AllowAlwaysGuard is the no-op DependencyGuard: it approves every // transition. Intended for tests, dev harnesses, and single-tenant // deployments where cross-decision dependencies are not modelled. // // Production deployments that care about session-level ordering MUST // supply their own DependencyGuard. staging.NewEngine panics on a // nil guard -- the zero-value fail-fast mirrors validator.Validator's // nil-panic contract (commit 1b0a860) and forces callers to // explicitly opt in to "no dependency constraints" by passing // AllowAlwaysGuard{} rather than silently accepting a nil field. // // AllowAlwaysGuard 是空操作 DependencyGuard: 每个 transition 都放行. // 用于测试 / dev 环境 / 不建模跨决策依赖的单租户部署. // // 关心 session 级顺序的生产部署必须自供 DependencyGuard. staging.NewEngine // 对 nil guard 直接 panic -- 零值 fail-fast 对齐 validator.Validator 的 // nil-panic 契约 (commit 1b0a860), 逼调用方显式传 AllowAlwaysGuard{} // 表达 "无依赖约束" 的意图, 而非静默接受 nil 字段. type AllowAlwaysGuard struct{} // AllowTransition always returns (true, nil). // // AllowTransition 始终返回 (true, nil). func (AllowAlwaysGuard) AllowTransition(_ context.Context, _ Record, _ State) (bool, error) { return true, nil } // TenantDenyGuard denies every transition whose Record.Metadata // carries a tenant id listed in BlockedTenants. This is the // canonical "metadata-driven guard" shape: the guard reads a // well-known key out of Record.Metadata (here, "tenant") and // decides based on a deny-list. Production deployments usually // extend or wrap this with their own dependency logic; it serves // as both a working example and a minimal building block. // // Metadata key: "tenant" (string). Missing / wrong-type value is // treated as "not on the deny-list" -- the guard only blocks when // it positively identifies a blocked tenant. // // TenantDenyGuard 拒绝所有 Record.Metadata 中 tenant id 在 // BlockedTenants 里的 transition. 这是典型的 "metadata-driven // guard" 形态: 从 Record.Metadata 读一个约定 key (这里是 "tenant") // 按 deny-list 决定. 生产部署通常以此为起点扩展或包装自己的依赖 // 逻辑; 既是可用示例, 也是最小构件. // // Metadata key: "tenant" (string). 缺失或类型不对视为 "不在 deny-list" // -- guard 仅在明确识别到被禁 tenant 时才拒. type TenantDenyGuard struct { // BlockedTenants maps tenant id to bool (true = blocked). // A nil or empty map denies nothing. // // BlockedTenants 把 tenant id 映射到 bool (true = 被禁). // nil 或空 map 不拒任何 tenant. BlockedTenants map[string]bool } // AllowTransition denies the transition when the Record's tenant // metadata is in BlockedTenants; allows otherwise. // // AllowTransition 在 Record 的 tenant metadata 位于 BlockedTenants // 时拒绝 transition; 其他情况放行. func (g TenantDenyGuard) AllowTransition(_ context.Context, r Record, _ State) (bool, error) { tenant, _ := r.Metadata["tenant"].(string) if g.BlockedTenants[tenant] { return false, nil } return true, nil }