package litigationplanner import "encoding/json" // allFlagsSet returns true when every element of `required` is present in // `set`. Empty `required` returns true (no condition). Retained as the // fallback predicate used by EvalConditionExpr when condition_expr is // NULL but the legacy condition_flag text[] is set — preserves // transition-window behaviour for any row Slice 2 missed (it shouldn't, // but defensive). func allFlagsSet(required []string, set map[string]struct{}) bool { for _, f := range required { if _, ok := set[f]; !ok { return false } } return true } // EvalConditionExpr returns true iff the rule's gate predicate is // satisfied for the caller's flag set. Drives flag-conditional rendering // + flag-conditional alt-swap throughout the calculator. // // Grammar (design §2.4 long form, mig 084 backfill): // // {"flag": ""} — leaf: true iff ∈ flags // {"op": "and", "args": [...]} — true iff every arg evaluates true // {"op": "or", "args": [...]} — true iff any arg evaluates true // {"op": "not", "args": []} — true iff the single arg is false // // NULL / empty / "null" expression → true (unconditional). Malformed // JSON → true (defensive: the rule still renders, the lawyer sees // it even if the gate is broken). // // Slice 9 (t-paliad-195, mig 091) dropped the legacy condition_flag // text[] column; the fallback that AND'd over it is gone. Any future // row needing array-of-flags semantics writes the equivalent // {"op":"and","args":[{"flag":""},...]} jsonb directly. func EvalConditionExpr(expr []byte, flags map[string]struct{}) bool { if len(expr) == 0 || string(expr) == "null" { return true } return EvalConditionExprNode(expr, flags) } // EvalConditionExprNode walks one node of the condition_expr jsonb // tree. Recursion depth is bounded by the editor (Slice 11 caps tree // depth + arg count); pre-Slice-11 backfilled rows have at most a // 2-arg AND (mig 084). func EvalConditionExprNode(raw []byte, flags map[string]struct{}) bool { var node struct { Flag string `json:"flag"` Op string `json:"op"` Args []json.RawMessage `json:"args"` } if err := json.Unmarshal(raw, &node); err != nil { // Malformed → unconditional. The Slice 11 editor's validation // will block such writes; in the live corpus today mig 084's // jsonb_build_object output is well-formed by construction. return true } if node.Flag != "" { _, ok := flags[node.Flag] return ok } switch node.Op { case "and": for _, a := range node.Args { if !EvalConditionExprNode(a, flags) { return false } } return true case "or": for _, a := range node.Args { if EvalConditionExprNode(a, flags) { return true } } return false case "not": if len(node.Args) != 1 { // Malformed NOT — fall through to unconditional rather // than risk suppressing a rule the lawyer expects to see. return true } return !EvalConditionExprNode(node.Args[0], flags) } // Unknown op (forward-compat with editor extensions): treat as // unconditional so the rule still renders. return true } // HasConditionExpr returns true when the rule carries a non-empty, // non-"null" jsonb gate. Slice 9 (t-paliad-195) replacement for the // pre-drop `len(r.ConditionFlag) > 0` predicate that guarded the // flag-keyed alt-swap branch. Same intent: "this rule has a gate; // when the gate flips to met, swap to alt". func HasConditionExpr(expr NullableJSON) bool { if len(expr) == 0 { return false } s := string(expr) return s != "null" && s != "{}" } // ExtractFlagsFromExpr walks the jsonb gate and returns the unique // flag names referenced as {"flag":""} leaves. Used by // CalculateRule's response (FlagsRequired) so the result-card calc // panel can render flag checkboxes for each gate input. Replaces the // dropped condition_flag text[] enumeration. Returns nil on a NULL // expression or one that contains no flag leaves. func ExtractFlagsFromExpr(expr NullableJSON) []string { if !HasConditionExpr(expr) { return nil } seen := make(map[string]struct{}) walkFlagLeaves([]byte(expr), seen) if len(seen) == 0 { return nil } out := make([]string, 0, len(seen)) for f := range seen { out = append(out, f) } return out } func walkFlagLeaves(raw []byte, into map[string]struct{}) { var node struct { Flag string `json:"flag"` Op string `json:"op"` Args []json.RawMessage `json:"args"` } if err := json.Unmarshal(raw, &node); err != nil { return } if node.Flag != "" { into[node.Flag] = struct{}{} return } for _, a := range node.Args { walkFlagLeaves(a, into) } }