mod: Phase 3 — lifecycle state machine + escalation + appeal, 106/106
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 54s
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>
This commit is contained in:
160
lib/mod/lifecycle.sx
Normal file
160
lib/mod/lifecycle.sx
Normal file
@@ -0,0 +1,160 @@
|
||||
;; 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)))))
|
||||
Reference in New Issue
Block a user