Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 54s
Pure SX state machine (lib/mod/lifecycle.sx) over the engine: open→triaged→decided→appealed→final, transition table guards illegal moves. Auto-tier resolves terminal actions; escalate parks at human-tier (resolve blocked until review supplies evidence). Appeal re-runs the engine — new exonerated-keep rule at top precedence lets exoneration override a prior hide. Api façade (mod/triage/resolve/review/appeal/finalize) over a case registry, logging committed decisions to the audit trail. +46 escalation tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
161 lines
4.6 KiB
Plaintext
161 lines
4.6 KiB
Plaintext
;; lib/mod/lifecycle.sx — report lifecycle state machine (pure SX over the engine).
|
|
;;
|
|
;; Lifecycle state is deliberately separate from policy: the Prolog rules answer
|
|
;; "what action?", this module answers "where in the process is this report?".
|
|
;;
|
|
;; :open ──triage──▶ :triaged ──resolve/review──▶ :decided ──appeal──▶ :appealed
|
|
;; │ │
|
|
;; └────finalize───▶ :final ◀┘
|
|
;;
|
|
;; A case is an immutable value {:report :state :decision :tier :error :history}.
|
|
;; Every transition returns a NEW case; illegal transitions return the case
|
|
;; unchanged with :error set. Tiers: triage runs the engine (auto-tier); a
|
|
;; terminal action (hide/remove/keep) resolves immediately, an :escalate action
|
|
;; flags the case for human review (human-tier) before it can be resolved.
|
|
|
|
(define mod/case* (fn (report state decision tier err history) {:history history :state state :report report :error err :tier tier :decision decision}))
|
|
|
|
(define
|
|
mod/mk-case
|
|
(fn (report) (mod/case* report "open" nil nil nil (list))))
|
|
|
|
(define mod/case-report (fn (c) (get c :report)))
|
|
(define mod/case-state (fn (c) (get c :state)))
|
|
(define mod/case-decision (fn (c) (get c :decision)))
|
|
(define mod/case-tier (fn (c) (get c :tier)))
|
|
(define mod/case-error (fn (c) (get c :error)))
|
|
(define mod/case-history (fn (c) (get c :history)))
|
|
|
|
;; ── transition table ──
|
|
|
|
(define mod/lc-transitions {:final (list) :appealed (list "final") :decided (list "appealed" "final") :open (list "triaged") :triaged (list "decided")})
|
|
|
|
(define mod/member? (fn (x lst) (mod/any? (fn (y) (= y x)) lst)))
|
|
|
|
(define
|
|
mod/lc-can-transition?
|
|
(fn
|
|
(from to)
|
|
(let
|
|
((outs (get mod/lc-transitions from)))
|
|
(if (nil? outs) false (mod/member? to outs)))))
|
|
|
|
;; ── core transition: validate, record history, or flag :error ──
|
|
|
|
(define
|
|
mod/case-goto
|
|
(fn
|
|
(c to note report decision tier)
|
|
(let
|
|
((from (mod/case-state c)))
|
|
(if
|
|
(mod/lc-can-transition? from to)
|
|
(mod/case*
|
|
report
|
|
to
|
|
decision
|
|
tier
|
|
nil
|
|
(append (mod/case-history c) (list {:note note :to to :from from})))
|
|
(mod/case*
|
|
(mod/case-report c)
|
|
from
|
|
(mod/case-decision c)
|
|
(mod/case-tier c)
|
|
(str "illegal transition: " from " -> " to)
|
|
(mod/case-history c))))))
|
|
|
|
(define
|
|
mod/case-error-set
|
|
(fn
|
|
(c msg)
|
|
(mod/case*
|
|
(mod/case-report c)
|
|
(mod/case-state c)
|
|
(mod/case-decision c)
|
|
(mod/case-tier c)
|
|
msg
|
|
(mod/case-history c))))
|
|
|
|
;; ── lifecycle operations ──
|
|
|
|
;; :open → :triaged — run the auto-tier first pass.
|
|
(define
|
|
mod/case-triage
|
|
(fn
|
|
(c reports rules)
|
|
(let
|
|
((d (mod/decide-report (mod/case-report c) reports rules)))
|
|
(let
|
|
((tier (if (= (get d :action) "escalate") "human" "auto")))
|
|
(mod/case-goto
|
|
c
|
|
"triaged"
|
|
"auto-tier first pass"
|
|
(mod/case-report c)
|
|
d
|
|
tier)))))
|
|
|
|
;; :triaged → :decided — auto-tier resolves; human-tier is blocked until review.
|
|
(define
|
|
mod/case-resolve
|
|
(fn
|
|
(c)
|
|
(if
|
|
(= (mod/case-tier c) "human")
|
|
(mod/case-error-set c "awaiting human review (escalated)")
|
|
(mod/case-goto
|
|
c
|
|
"decided"
|
|
"auto-tier resolved"
|
|
(mod/case-report c)
|
|
(mod/case-decision c)
|
|
(mod/case-tier c)))))
|
|
|
|
;; :triaged → :decided — human review: attach evidence, re-decide, resolve.
|
|
(define
|
|
mod/case-review
|
|
(fn
|
|
(c kind val reports rules)
|
|
(let
|
|
((nr (mod/attach-evidence (mod/case-report c) (mod/mk-evidence kind val))))
|
|
(let
|
|
((d (mod/decide-report nr reports rules)))
|
|
(mod/case-goto c "decided" (str "human review: " kind) nr d "human")))))
|
|
|
|
;; :decided → :appealed — appeal: attach evidence, re-decide (may override).
|
|
(define
|
|
mod/case-appeal
|
|
(fn
|
|
(c kind val reports rules)
|
|
(let
|
|
((nr (mod/attach-evidence (mod/case-report c) (mod/mk-evidence kind val))))
|
|
(let
|
|
((d (mod/decide-report nr reports rules)))
|
|
(mod/case-goto
|
|
c
|
|
"appealed"
|
|
(str "appeal: " kind)
|
|
nr
|
|
d
|
|
(mod/case-tier c))))))
|
|
|
|
;; :decided | :appealed → :final
|
|
(define
|
|
mod/case-finalize
|
|
(fn
|
|
(c)
|
|
(mod/case-goto
|
|
c
|
|
"final"
|
|
"finalized"
|
|
(mod/case-report c)
|
|
(mod/case-decision c)
|
|
(mod/case-tier c))))
|
|
|
|
(define
|
|
mod/case-action
|
|
(fn
|
|
(c)
|
|
(let ((d (mod/case-decision c))) (if (nil? d) nil (get d :action)))))
|