diff --git a/lib/mod/conformance.conf b/lib/mod/conformance.conf index 9f348d59..e558811f 100644 --- a/lib/mod/conformance.conf +++ b/lib/mod/conformance.conf @@ -21,6 +21,7 @@ PRELOADS=( lib/mod/whatif.sx lib/mod/batch.sx lib/mod/temporal.sx + lib/mod/sla.sx lib/mod/lifecycle.sx lib/mod/audit.sx lib/mod/api.sx @@ -44,4 +45,5 @@ SUITES=( "whatif:lib/mod/tests/whatif.sx:(mod-whatif-tests-run!)" "batch:lib/mod/tests/batch.sx:(mod-batch-tests-run!)" "temporal:lib/mod/tests/temporal.sx:(mod-temporal-tests-run!)" + "sla:lib/mod/tests/sla.sx:(mod-sla-tests-run!)" ) diff --git a/lib/mod/scoreboard.json b/lib/mod/scoreboard.json index 40cf2c07..97f2beb8 100644 --- a/lib/mod/scoreboard.json +++ b/lib/mod/scoreboard.json @@ -1,8 +1,8 @@ { "lang": "mod", - "total_passed": 292, + "total_passed": 307, "total_failed": 0, - "total": 292, + "total": 307, "suites": [ {"name":"decide","passed":31,"failed":0,"total":31}, {"name":"audit","passed":29,"failed":0,"total":29}, @@ -17,7 +17,8 @@ {"name":"trace","passed":15,"failed":0,"total":15}, {"name":"whatif","passed":13,"failed":0,"total":13}, {"name":"batch","passed":17,"failed":0,"total":17}, - {"name":"temporal","passed":15,"failed":0,"total":15} + {"name":"temporal","passed":15,"failed":0,"total":15}, + {"name":"sla","passed":15,"failed":0,"total":15} ], - "generated": "2026-06-06T19:00:19+00:00" + "generated": "2026-06-06T19:08:06+00:00" } diff --git a/lib/mod/scoreboard.md b/lib/mod/scoreboard.md index f66956bc..680ef546 100644 --- a/lib/mod/scoreboard.md +++ b/lib/mod/scoreboard.md @@ -1,6 +1,6 @@ # mod scoreboard -**292 / 292 passing** (0 failure(s)). +**307 / 307 passing** (0 failure(s)). | Suite | Passed | Total | Status | |-------|--------|-------|--------| @@ -18,3 +18,4 @@ | whatif | 13 | 13 | ok | | batch | 17 | 17 | ok | | temporal | 15 | 15 | ok | +| sla | 15 | 15 | ok | diff --git a/lib/mod/sla.sx b/lib/mod/sla.sx new file mode 100644 index 00000000..4a437957 --- /dev/null +++ b/lib/mod/sla.sx @@ -0,0 +1,47 @@ +;; lib/mod/sla.sx — service-level sweep over pending lifecycle cases. +;; +;; Composes the Phase-3 lifecycle with the Ext-12 time dimension: a case left in a +;; pending state (open / triaged / appealed) past a deadline has breached SLA and +;; should resurface. A timed-case pairs a case with the tick it entered its +;; current state (the caller stamps this — the lifecycle stays timeless and pure). +;; Terminal states (decided / final) never breach. + +(define mod/pending-states (list "open" "triaged" "appealed")) +(define mod/pending-state? (fn (s) (mod/member? s mod/pending-states))) + +(define mod/mk-timed-case (fn (c entered-at) {:entered-at entered-at :case c})) +(define mod/tc-case (fn (tc) (get tc :case))) +(define mod/tc-entered-at (fn (tc) (get tc :entered-at))) + +(define + mod/overdue? + (fn + (tc now deadline) + (if + (mod/pending-state? (mod/case-state (mod/tc-case tc))) + (< deadline (- now (mod/tc-entered-at tc))) + false))) + +(define + mod/sla-sweep + (fn + (timed-cases now deadline) + (reduce + (fn + (acc tc) + (if + (mod/overdue? tc now deadline) + (append + acc + (list (mod/report-id (mod/case-report (mod/tc-case tc))))) + acc)) + (list) + timed-cases))) + +(define + mod/overdue-count + (fn + (timed-cases now deadline) + (len (mod/sla-sweep timed-cases now deadline)))) + +(define mod/age (fn (tc now) (- now (mod/tc-entered-at tc)))) diff --git a/lib/mod/tests/sla.sx b/lib/mod/tests/sla.sx new file mode 100644 index 00000000..ccf50ec9 --- /dev/null +++ b/lib/mod/tests/sla.sx @@ -0,0 +1,108 @@ +;; lib/mod/tests/sla.sx — Ext 13: SLA sweep over pending lifecycle cases. + +(define mod-sla-count 0) +(define mod-sla-pass 0) +(define mod-sla-fail 0) +(define mod-sla-failures (list)) + +(define + mod-sla-test! + (fn + (name got expected) + (begin + (set! mod-sla-count (+ mod-sla-count 1)) + (if + (= got expected) + (set! mod-sla-pass (+ mod-sla-pass 1)) + (begin + (set! mod-sla-fail (+ mod-sla-fail 1)) + (append! + mod-sla-failures + (str name "\n expected: " expected "\n got: " got))))))) + +;; ── pending-state? ── + +(mod-sla-test! "open is pending" (mod/pending-state? "open") true) +(mod-sla-test! "triaged is pending" (mod/pending-state? "triaged") true) +(mod-sla-test! "appealed is pending" (mod/pending-state? "appealed") true) +(mod-sla-test! "decided is not pending" (mod/pending-state? "decided") false) +(mod-sla-test! "final is not pending" (mod/pending-state? "final") false) + +;; build cases in known states +(define mod-sla-spam (mod/mk-report "r1" "u" "bob" "this is spam")) +(define mod-sla-spam-reports (list mod-sla-spam)) +(define + mod-sla-triaged + (mod/case-triage + (mod/mk-case mod-sla-spam) + mod-sla-spam-reports + mod/default-rules)) +(define mod-sla-decided (mod/case-resolve mod-sla-triaged)) +(define mod-sla-open (mod/mk-case (mod/mk-report "r2" "u" "eve" "hello"))) + +;; ── overdue? ── + +(define mod-sla-tc-old (mod/mk-timed-case mod-sla-triaged 0)) +(define mod-sla-tc-fresh (mod/mk-timed-case mod-sla-triaged 90)) +(define mod-sla-tc-done (mod/mk-timed-case mod-sla-decided 0)) + +(mod-sla-test! + "old triaged case is overdue" + (mod/overdue? mod-sla-tc-old 100 50) + true) +(mod-sla-test! + "fresh triaged case not overdue" + (mod/overdue? mod-sla-tc-fresh 100 50) + false) +(mod-sla-test! + "decided case never overdue" + (mod/overdue? mod-sla-tc-done 100 50) + false) +(mod-sla-test! + "age computes elapsed ticks" + (mod/age mod-sla-tc-old 100) + 100) +(mod-sla-test! + "boundary: exactly at deadline not overdue" + (mod/overdue? + (mod/mk-timed-case mod-sla-triaged 50) + 100 + 50) + false) +(mod-sla-test! + "boundary: one past deadline overdue" + (mod/overdue? + (mod/mk-timed-case mod-sla-triaged 49) + 100 + 50) + true) + +;; ── sweep over a mixed queue ── + +(define + mod-sla-queue + (list + (mod/mk-timed-case mod-sla-triaged 0) + (mod/mk-timed-case mod-sla-decided 0) + (mod/mk-timed-case mod-sla-open 90))) ;; r2, pending, age 10 → not + +(mod-sla-test! + "sweep finds only the overdue pending case" + (mod/sla-sweep mod-sla-queue 100 50) + (list "r1")) +(mod-sla-test! + "overdue-count agrees" + (mod/overdue-count mod-sla-queue 100 50) + 1) + +;; tighten deadline so the young open case also breaches +(mod-sla-test! + "tighter deadline catches the open case too" + (mod/overdue-count mod-sla-queue 100 5) + 2) +(mod-sla-test! + "empty queue → no breaches" + (mod/sla-sweep (list) 100 50) + (list)) + +(define mod-sla-tests-run! (fn () {:failures mod-sla-failures :total mod-sla-count :passed mod-sla-pass :failed mod-sla-fail})) diff --git a/plans/mod-on-sx.md b/plans/mod-on-sx.md index 0d394230..742919eb 100644 --- a/plans/mod-on-sx.md +++ b/plans/mod-on-sx.md @@ -16,7 +16,7 @@ federation extension. ## Status (rolling) -`bash lib/mod/conformance.sh` → **292/292** (roadmap + 12 extensions complete) +`bash lib/mod/conformance.sh` → **307/307** (roadmap + 13 extensions complete) ## Ground rules @@ -147,6 +147,11 @@ lib/mod/fed.sx derivation goal-by-goal with `[proved]`/`[unproved]` marks and unification bindings. E.g. `Report rc: escalate (rule: repeated-escalate)` … `[proved] report(rc, B, S), report_count(S, N), N >= 3 {B=ann, N=3, S=dave}`. +- [x] **Ext 13 — SLA sweep over pending cases** (`lib/mod/sla.sx`, +15). Composes + lifecycle (Phase 3) with time (Ext 12): a timed-case pairs a case with the tick + it entered its state; `mod/overdue?` flags pending cases (open/triaged/appealed) + past a deadline; `mod/sla-sweep` returns the breached report ids. Terminal states + never breach. Pure overlay — lifecycle stays timeless, the caller stamps entry. - [x] **Ext 12 — temporal burst detection** (`lib/mod/temporal.sx`, +15). Reports gain an `:at` tick (deterministic, supplied — never clock-read). `mod/decide-temporal now window` counts reports about the subject within @@ -202,6 +207,11 @@ lib/mod/fed.sx ## Progress log +- **Ext 13 — SLA sweep, 307/307** (+15). Two subsystems compose cleanly: lifecycle + states + temporal ticks → "which pending cases have sat too long". Kept lifecycle + pure by having the SLA layer carry entry-time externally (timed-case wrapper) + rather than stamping the case — same separation-of-concerns as keeping the state + machine out of Prolog. - **Ext 12 — temporal burst detection, 292/292** (+15). Adds the time dimension: a windowed count distinguishes a burst from slow accumulation, where the plain count rule cannot. Time is a supplied tick (`:at`), keeping everything