mod: Ext 9 — policy dry-run trace diagnostics, 247/247
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 49s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 49s
mod/trace-rules evaluates a report against every rule, returning each rule's proved/unproved status + goal-by-goal derivation (an unproved rule shows which goal failed). mod/first-proved = winner (matches engine precedence, cross-checked), mod/proved-rules the firing set, mod/trace-report a [fires]/[ - ] rendering. Answers 'why didn't my rule fire?' without instrumenting the engine. +15 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ PRELOADS=(
|
||||
lib/mod/severity.sx
|
||||
lib/mod/offenders.sx
|
||||
lib/mod/quorum.sx
|
||||
lib/mod/trace.sx
|
||||
lib/mod/lifecycle.sx
|
||||
lib/mod/audit.sx
|
||||
lib/mod/api.sx
|
||||
@@ -36,4 +37,5 @@ SUITES=(
|
||||
"severity:lib/mod/tests/severity.sx:(mod-severity-tests-run!)"
|
||||
"offenders:lib/mod/tests/offenders.sx:(mod-offenders-tests-run!)"
|
||||
"quorum:lib/mod/tests/quorum.sx:(mod-quorum-tests-run!)"
|
||||
"trace:lib/mod/tests/trace.sx:(mod-trace-tests-run!)"
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"lang": "mod",
|
||||
"total_passed": 232,
|
||||
"total_passed": 247,
|
||||
"total_failed": 0,
|
||||
"total": 232,
|
||||
"total": 247,
|
||||
"suites": [
|
||||
{"name":"decide","passed":31,"failed":0,"total":31},
|
||||
{"name":"audit","passed":29,"failed":0,"total":29},
|
||||
@@ -13,7 +13,8 @@
|
||||
{"name":"lint","passed":14,"failed":0,"total":14},
|
||||
{"name":"severity","passed":14,"failed":0,"total":14},
|
||||
{"name":"offenders","passed":19,"failed":0,"total":19},
|
||||
{"name":"quorum","passed":9,"failed":0,"total":9}
|
||||
{"name":"quorum","passed":9,"failed":0,"total":9},
|
||||
{"name":"trace","passed":15,"failed":0,"total":15}
|
||||
],
|
||||
"generated": "2026-06-06T18:44:47+00:00"
|
||||
"generated": "2026-06-06T18:48:10+00:00"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mod scoreboard
|
||||
|
||||
**232 / 232 passing** (0 failure(s)).
|
||||
**247 / 247 passing** (0 failure(s)).
|
||||
|
||||
| Suite | Passed | Total | Status |
|
||||
|-------|--------|-------|--------|
|
||||
@@ -14,3 +14,4 @@
|
||||
| severity | 14 | 14 | ok |
|
||||
| offenders | 19 | 19 | ok |
|
||||
| quorum | 9 | 9 | ok |
|
||||
| trace | 15 | 15 | ok |
|
||||
|
||||
116
lib/mod/tests/trace.sx
Normal file
116
lib/mod/tests/trace.sx
Normal file
@@ -0,0 +1,116 @@
|
||||
;; lib/mod/tests/trace.sx — Ext 9: policy dry-run diagnostics.
|
||||
|
||||
(define mod-tr-count 0)
|
||||
(define mod-tr-pass 0)
|
||||
(define mod-tr-fail 0)
|
||||
(define mod-tr-failures (list))
|
||||
|
||||
(define
|
||||
mod-tr-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! mod-tr-count (+ mod-tr-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! mod-tr-pass (+ mod-tr-pass 1))
|
||||
(begin
|
||||
(set! mod-tr-fail (+ mod-tr-fail 1))
|
||||
(append!
|
||||
mod-tr-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
mod-tr-find
|
||||
(fn
|
||||
(trace nm)
|
||||
(reduce (fn (acc t) (if (= (get t :rule) nm) t acc)) nil trace)))
|
||||
|
||||
;; ── trace a spam report against the default rules ──
|
||||
|
||||
(define mod-tr-spam (mod/mk-report "r1" "alice" "bob" "this is spam"))
|
||||
(define
|
||||
mod-tr-t
|
||||
(mod/trace-rules mod-tr-spam (list mod-tr-spam) mod/default-rules))
|
||||
|
||||
(mod-tr-test! "trace covers every rule" (len mod-tr-t) 6)
|
||||
(mod-tr-test!
|
||||
"spam-hide fires"
|
||||
(get (mod-tr-find mod-tr-t "spam-hide") :proved)
|
||||
true)
|
||||
(mod-tr-test!
|
||||
"default-keep always fires"
|
||||
(get (mod-tr-find mod-tr-t "default-keep") :proved)
|
||||
true)
|
||||
(mod-tr-test!
|
||||
"reviewer-remove does not fire (no evidence)"
|
||||
(get (mod-tr-find mod-tr-t "reviewer-remove") :proved)
|
||||
false)
|
||||
(mod-tr-test!
|
||||
"exonerated-keep does not fire"
|
||||
(get (mod-tr-find mod-tr-t "exonerated-keep") :proved)
|
||||
false)
|
||||
(mod-tr-test!
|
||||
"abuse-remove does not fire"
|
||||
(get (mod-tr-find mod-tr-t "abuse-remove") :proved)
|
||||
false)
|
||||
|
||||
;; ── winner matches the engine ──
|
||||
|
||||
(mod-tr-test!
|
||||
"first-proved is spam-hide"
|
||||
(get (mod/first-proved mod-tr-t) :rule)
|
||||
"spam-hide")
|
||||
(mod-tr-test!
|
||||
"winner action matches decide-report"
|
||||
(get (mod/first-proved mod-tr-t) :action)
|
||||
(get
|
||||
(mod/decide-report mod-tr-spam (list mod-tr-spam) mod/default-rules)
|
||||
:action))
|
||||
|
||||
;; ── an unproved rule shows which goal failed ──
|
||||
|
||||
(define
|
||||
mod-tr-rev-goals
|
||||
(get (mod-tr-find mod-tr-t "reviewer-remove") :goals))
|
||||
(mod-tr-test!
|
||||
"reviewer-remove goal is unsolved"
|
||||
(get (first mod-tr-rev-goals) :solved)
|
||||
false)
|
||||
(define mod-tr-spam-goals (get (mod-tr-find mod-tr-t "spam-hide") :goals))
|
||||
(mod-tr-test!
|
||||
"spam-hide goal is solved"
|
||||
(get (first mod-tr-spam-goals) :solved)
|
||||
true)
|
||||
|
||||
;; ── proved-rules list + rendering ──
|
||||
|
||||
(mod-tr-test!
|
||||
"proved-rules lists fired rules in order"
|
||||
(mod/proved-rules mod-tr-t)
|
||||
(list "spam-hide" "default-keep"))
|
||||
(mod-tr-test!
|
||||
"trace-report marks a firing rule"
|
||||
(mod/str-contains? (mod/trace-report mod-tr-t) "[fires] spam-hide")
|
||||
true)
|
||||
(mod-tr-test!
|
||||
"trace-report marks a non-firing rule"
|
||||
(mod/str-contains? (mod/trace-report mod-tr-t) "[ - ] reviewer-remove")
|
||||
true)
|
||||
|
||||
;; ── clean report: only default-keep fires ──
|
||||
|
||||
(define mod-tr-clean (mod/mk-report "r2" "a" "b" "a fine post"))
|
||||
(define
|
||||
mod-tr-tc
|
||||
(mod/trace-rules mod-tr-clean (list mod-tr-clean) mod/default-rules))
|
||||
(mod-tr-test!
|
||||
"clean report: only default-keep proves"
|
||||
(mod/proved-rules mod-tr-tc)
|
||||
(list "default-keep"))
|
||||
(mod-tr-test!
|
||||
"clean report winner is default-keep"
|
||||
(get (mod/first-proved mod-tr-tc) :rule)
|
||||
"default-keep")
|
||||
|
||||
(define mod-trace-tests-run! (fn () {:failures mod-tr-failures :total mod-tr-count :passed mod-tr-pass :failed mod-tr-fail}))
|
||||
56
lib/mod/trace.sx
Normal file
56
lib/mod/trace.sx
Normal file
@@ -0,0 +1,56 @@
|
||||
;; lib/mod/trace.sx — policy dry-run diagnostics.
|
||||
;;
|
||||
;; decide-report returns the winning rule; a policy author debugging "why didn't
|
||||
;; my rule fire?" needs the whole picture. mod/trace-rules evaluates a report
|
||||
;; against every rule and reports each rule's proved/unproved status plus its
|
||||
;; goal-by-goal derivation — so an unproved rule shows exactly which goal failed.
|
||||
;; The winner is the first proved rule (same precedence as the engine).
|
||||
|
||||
(define
|
||||
mod/trace-rules
|
||||
(fn
|
||||
(r reports rules)
|
||||
(let
|
||||
((count (mod/report-count (mod/report-about r) reports))
|
||||
(id (mod/report-id r)))
|
||||
(let
|
||||
((db (pl-load (mod/build-program r count rules))))
|
||||
(let
|
||||
((proved-names (map (fn (s) (dict-get s "Rule")) (pl-query-all db (str "policy_action(" id ", _, Rule)")))))
|
||||
(map
|
||||
(fn (rule) (let ((nm (mod/rule-name rule))) {:proved (mod/member? nm proved-names) :goals (mod/proof-goals db id (mod/rule-when rule)) :action (mod/rule-action rule) :rule nm}))
|
||||
rules))))))
|
||||
|
||||
(define
|
||||
mod/first-proved
|
||||
(fn
|
||||
(trace)
|
||||
(reduce
|
||||
(fn (acc t) (if (nil? acc) (if (get t :proved) t acc) acc))
|
||||
nil
|
||||
trace)))
|
||||
|
||||
(define
|
||||
mod/proved-rules
|
||||
(fn
|
||||
(trace)
|
||||
(reduce
|
||||
(fn
|
||||
(acc t)
|
||||
(if (get t :proved) (append acc (list (get t :rule))) acc))
|
||||
(list)
|
||||
trace)))
|
||||
|
||||
(define
|
||||
mod/trace-row
|
||||
(fn
|
||||
(t)
|
||||
(str
|
||||
(if (get t :proved) "[fires] " "[ - ] ")
|
||||
(get t :rule)
|
||||
" → "
|
||||
(get t :action))))
|
||||
|
||||
(define
|
||||
mod/trace-report
|
||||
(fn (trace) (mod/join-with "\n" (map mod/trace-row trace))))
|
||||
@@ -16,7 +16,7 @@ federation extension.
|
||||
|
||||
## Status (rolling)
|
||||
|
||||
`bash lib/mod/conformance.sh` → **232/232** (roadmap + 8 extensions complete)
|
||||
`bash lib/mod/conformance.sh` → **247/247** (roadmap + 9 extensions complete)
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -147,6 +147,12 @@ 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 9 — policy dry-run trace** (`lib/mod/trace.sx`, +15). `mod/trace-rules`
|
||||
evaluates a report against every rule and returns each rule's proved/unproved
|
||||
status + its goal-by-goal derivation, so an unproved rule shows which goal
|
||||
failed. `mod/first-proved` = the winner (engine precedence), `mod/proved-rules`
|
||||
the full firing set, `mod/trace-report` a `[fires]`/`[ - ]` rendering. Answers
|
||||
"why didn't my rule fire?" without instrumenting the engine.
|
||||
- [x] **Ext 8 — quorum over distinct reporters** (`lib/mod/quorum.sx`, +9). Anti-
|
||||
brigade: `(:reporters-at-least N)` compiles to `setof(Br, report(_, Br, Sr), Bsr),
|
||||
length(Bsr, Nr), Nr >= N` — distinct reporters, not raw report count.
|
||||
@@ -180,6 +186,11 @@ lib/mod/fed.sx
|
||||
|
||||
## Progress log
|
||||
|
||||
- **Ext 9 — policy dry-run trace, 247/247** (+15). Whole-rule-set diagnostics over
|
||||
the proof machinery: every rule's fire/no-fire and the goal that decided it. The
|
||||
winner agrees with `decide-report` by construction (first proved = pl-query-one),
|
||||
cross-checked in a test. Turns the proof tree from a per-decision artifact into a
|
||||
policy-debugging tool.
|
||||
- **Ext 8 — quorum over distinct reporters, 232/232** (+9). Distinct-reporter
|
||||
consensus via Prolog `setof`/`length`, requiring a second engine variant that
|
||||
asserts all reports (the base engine deliberately scopes facts to the decided
|
||||
|
||||
Reference in New Issue
Block a user