feat: add bounded workflow decisions and durable result inbox

This commit is contained in:
2026-09-18 01:11:42 +08:00
parent 520e98d9d1
commit 0f7ac5203b
20 changed files with 1909 additions and 9 deletions

208
weave/engine/evaluate.go Normal file
View File

@@ -0,0 +1,208 @@
// Package engine 依据已发布规则和持久化快照计算有限推进命令;不调用业务处理器或外部系统。
package engine
import (
"bytes"
"encoding/json"
"errors"
"math/big"
"strings"
"git.yuntuo.hk/k2safe/orbit/internal/jsonwire"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
)
type values map[string]json.RawMessage
func copyJSON(raw json.RawMessage) json.RawMessage { return append(json.RawMessage(nil), raw...) }
func copyValues(in values) values {
out := make(values, len(in)+8)
for k, v := range in {
out[k] = v
}
return out
}
// 表达式来自已验证且由Program持有的不可变定义;每个值仍按运行数据上限检查。
func resolve(v definition.Value, expected definition.Type, env values) (json.RawMessage, error) {
raw, err := evaluate(v, env, 0)
if err != nil {
return nil, err
}
if err := definition.ValidateData(expected, raw); err != nil {
return nil, err
}
return raw, nil
}
func evaluate(v definition.Value, env values, depth int) (json.RawMessage, error) {
if depth > 8 {
return nil, errors.New("expression depth exceeded")
}
var out json.RawMessage
switch v.Kind {
case "LITERAL":
out = copyJSON(v.Literal)
case "REFERENCE":
if v.Reference == nil {
return nil, errors.New("reference required")
}
raw, ok := env[v.Reference.Source]
if !ok {
return nil, errors.New("reference unavailable")
}
for _, key := range v.Reference.Path {
var object map[string]json.RawMessage
if json.Unmarshal(raw, &object) != nil {
return nil, errors.New("reference is not an object")
}
raw, ok = object[key]
if !ok {
return nil, errors.New("reference field missing")
}
}
out = copyJSON(raw)
case "OBJECT":
object := make(map[string]json.RawMessage, len(v.Fields))
size := 0
for k, child := range v.Fields {
raw, err := evaluate(child, env, depth+1)
if err != nil {
return nil, err
}
size += len(raw)
if size > definition.MaxSpecificationBytes {
return nil, errors.New("resolved object exceeds bounds")
}
object[k] = raw
}
raw, err := jsonwire.Marshal(object)
if err != nil {
return nil, err
}
out = raw
case "ARRAY":
array := make([]json.RawMessage, len(v.Items))
size := 0
for i, child := range v.Items {
raw, err := evaluate(child, env, depth+1)
if err != nil {
return nil, err
}
size += len(raw)
if size > definition.MaxSpecificationBytes {
return nil, errors.New("resolved array exceeds bounds")
}
array[i] = raw
}
raw, err := jsonwire.Marshal(array)
if err != nil {
return nil, err
}
out = raw
default:
return nil, errors.New("unknown expression")
}
if len(out) == 0 || len(out) > definition.MaxSpecificationBytes {
return nil, errors.New("resolved value exceeds bounds")
}
return out, nil
}
func compare(p definition.Predicate, env values) (bool, error) {
switch p.Op {
case "AND", "OR", "NOT":
for _, child := range p.Arguments {
ok, err := compare(child, env)
if err != nil {
return false, err
}
if p.Op == "NOT" {
return !ok, nil
}
if p.Op == "AND" && !ok {
return false, nil
}
if p.Op == "OR" && ok {
return true, nil
}
}
return p.Op == "AND", nil
}
if p.Left == nil || p.Right == nil {
return false, errors.New("comparison operands missing")
}
left, err := resolve(*p.Left, definition.Type{Kind: "JSON"}, env)
if err != nil {
return false, err
}
right, err := resolve(*p.Right, definition.Type{Kind: "JSON"}, env)
if err != nil {
return false, err
}
decode := func(raw []byte) (any, error) {
var v any
d := json.NewDecoder(bytes.NewReader(raw))
d.UseNumber()
err := d.Decode(&v)
return v, err
}
a, err := decode(left)
if err != nil {
return false, err
}
b, err := decode(right)
if err != nil {
return false, err
}
var order int
switch v := a.(type) {
case string:
w, ok := b.(string)
if !ok {
return false, errors.New("comparison type mismatch")
}
order = strings.Compare(v, w)
case json.Number:
w, ok := b.(json.Number)
if !ok {
return false, errors.New("comparison type mismatch")
}
x, ok := new(big.Rat).SetString(string(v))
if !ok {
return false, errors.New("invalid exact number")
}
y, ok := new(big.Rat).SetString(string(w))
if !ok {
return false, errors.New("invalid exact number")
}
order = x.Cmp(y)
case bool:
w, ok := b.(bool)
if !ok || p.Op != "EQ" && p.Op != "NE" {
return false, errors.New("comparison type mismatch")
}
if v != w {
order = 1
}
case nil:
if b != nil || p.Op != "EQ" && p.Op != "NE" {
return false, errors.New("comparison type mismatch")
}
default:
return false, errors.New("comparison requires scalars")
}
switch p.Op {
case "EQ":
return order == 0, nil
case "NE":
return order != 0, nil
case "LT":
return order < 0, nil
case "LE":
return order <= 0, nil
case "GT":
return order > 0, nil
case "GE":
return order >= 0, nil
}
return false, errors.New("unknown comparison")
}