;; lib/mod/temporal.sx — burst detection over a time window. ;; ;; A plain report count can't tell a burst (N reports in minutes) from slow ;; accumulation (N reports over months). mod/decide-temporal takes a `now` tick ;; and a `window`, counts reports about the subject with :at within [now-window, ;; now], asserts it as burst_count/2, and lets a `(:burst-at-least K)` rule fire ;; only on a genuine burst. Time is supplied (deterministic), never clock-read. (define mod/window-count (fn (subject reports now window) (reduce (fn (acc r) (if (if (= (mod/report-about r) subject) (<= (- now window) (mod/report-at r)) false) (+ acc 1) acc)) 0 reports))) (define mod/build-temporal-program (fn (r count bcount rules) (str (mod/report-facts r count) "\n" "burst_count(" (mod/pl-quote (mod/report-about r)) ", " bcount ").\n" (mod/rules->program rules)))) (define mod/decide-temporal (fn (r reports rules now window) (let ((about (mod/report-about r)) (id (mod/report-id r)) (kinds (mod/classify-keywords r))) (let ((count (mod/report-count about reports)) (bcount (mod/window-count about reports now window))) (let ((program (mod/build-temporal-program r count bcount rules))) (let ((db (pl-load program))) (let ((sol (pl-query-one db (str "policy_action(" id ", Action, Rule)")))) (if (nil? sol) {:action "keep" :proof {:burst bcount :goals (list) :evidence kinds :conditions (list) :rule "none" :count count} :report-id id :rule "none" :strategy "temporal"} (let ((rule (mod/find-rule rules (dict-get sol "Rule")))) {:action (mod/rule-action rule) :proof {:burst bcount :goals (mod/proof-goals db id (mod/rule-when rule)) :evidence kinds :conditions (mod/rule-when rule) :rule (mod/rule-name rule) :count count} :report-id id :rule (mod/rule-name rule) :strategy "temporal"})))))))))