Skip to main content

qualia_core_db/modalities/
defeasible.rs

1use crate::NQuin;
2
3pub const OP_DEFEASIBLE_OVERRIDE: u8 = 0x50;
4// Shared with deontic logic; canonical bit position lives in the FrameLayout ABI.
5pub use crate::frame_layout::DEFEATER_BIT;
6
7#[derive(Debug, PartialEq, Clone, Copy)]
8pub enum DefeasibleStatus {
9    Strict,     // No exceptions allowed
10    Overridden, // Defeater proved true
11    Defeated,   // Normal rule defeated
12    Active,     // Normal rule stands
13}
14
15#[derive(Debug)]
16pub enum DefeasibleError {
17    BufferOverflow,
18}
19
20#[derive(Debug, Clone, Copy)]
21pub struct DefeasibleVerdict {
22    pub claim: NQuin,
23    pub status: DefeasibleStatus,
24}
25
26/// Evaluates a slice of Quins for non-monotonic (defeasible) reasoning.
27pub fn evaluate_defeasible_frame(
28    quins: &[NQuin],
29    context_hash: u64,
30    out: &mut [DefeasibleVerdict],
31) -> Result<usize, DefeasibleError> {
32    let mut count = 0;
33
34    for q in quins {
35        if context_hash != 0 && q.context != context_hash {
36            continue;
37        }
38
39        let is_defeater = (q.predicate & DEFEATER_BIT) != 0;
40        let opcode = (q.predicate & 0xFF) as u8;
41
42        let status = if is_defeater {
43            DefeasibleStatus::Overridden
44        } else if opcode == OP_DEFEASIBLE_OVERRIDE {
45            DefeasibleStatus::Defeated
46        } else {
47            DefeasibleStatus::Active
48        };
49
50        if count >= out.len() {
51            return Err(DefeasibleError::BufferOverflow);
52        }
53
54        out[count] = DefeasibleVerdict { claim: *q, status };
55        count += 1;
56    }
57
58    Ok(count)
59}
60
61/// Negation-as-failure / closed-world assumption: a proposition holds "by default"
62/// (its negation is concluded) exactly when it CANNOT be proven from the closed set
63/// of `facts` — i.e. the `(subject, predicate, object)` triple is absent. This is
64/// the non-monotonic primitive the positive forward-chainer (`fire_guard_rules`)
65/// cannot express; the agent-honesty guard's "Unverified until proven" is an
66/// instance. Zero-heap (single linear scan).
67pub fn holds_by_default(facts: &[NQuin], goal: &NQuin) -> bool {
68    !facts.iter().any(|q| {
69        q.subject == goal.subject && q.predicate == goal.predicate && q.object == goal.object
70    })
71}
72
73// ─── Defeasible Logic: strict / defeasible / defeater rules + superiority ───────────
74//
75// Defeasible Logic (Nute / Governatori) layers three rule kinds and a superiority relation:
76//   * STRICT rules — indefeasible (their conclusion always holds when fired).
77//   * DEFEASIBLE rules — hold unless defeated by a superior opposing rule.
78//   * DEFEATERS — cannot conclude on their own; they only BLOCK an opposing defeasible rule.
79// When two rules conclude complementary literals, the superiority relation decides; if neither
80// is superior the outcome is ambiguous, handled either by blocking or propagating ambiguity.
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum RuleKind {
84    Strict,
85    Defeasible,
86    Defeater,
87}
88
89/// A minimal defeasible rule: an id, its kind, the literal it concludes, and the polarity
90/// (`positive`: concludes `literal`; else concludes `¬literal`).
91#[derive(Debug, Clone, Copy)]
92pub struct DefeasibleRule {
93    pub id: u64,
94    pub kind: RuleKind,
95    pub literal: u64,
96    pub positive: bool,
97}
98
99/// How unresolved ambiguity (neither conflicting rule superior) is treated.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum AmbiguityMode {
102    /// Ambiguity is **blocked**: neither conclusion is drawn (`Undecided`).
103    Blocking,
104    /// Ambiguity is **propagated**: the literal is marked `Ambiguous` downstream.
105    Propagating,
106}
107
108/// The conclusion drawn for a literal after conflict resolution.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum Conclusion {
111    Positive,
112    Negative,
113    Ambiguous,
114    Undecided,
115}
116
117/// Two rules **conflict** iff they conclude the same literal with opposite polarity.
118pub fn rules_conflict(a: &DefeasibleRule, b: &DefeasibleRule) -> bool {
119    a.literal == b.literal && a.positive != b.positive
120}
121
122/// Superiority lookup: is `a` superior to `b` in the supplied relation `sup` (pairs
123/// `(higher, lower)`)?
124pub fn is_superior(sup: &[(u64, u64)], a: u64, b: u64) -> bool {
125    sup.iter().any(|&(hi, lo)| hi == a && lo == b)
126}
127
128#[inline]
129fn polarity(r: &DefeasibleRule) -> Conclusion {
130    if r.positive {
131        Conclusion::Positive
132    } else {
133        Conclusion::Negative
134    }
135}
136
137/// A `Defeater` can only block an opponent — it never supports a conclusion. Strict and
138/// defeasible rules can conclude.
139#[inline]
140fn can_conclude(kind: RuleKind) -> bool {
141    matches!(kind, RuleKind::Strict | RuleKind::Defeasible)
142}
143
144/// Resolve a conflict between two opposing rules, given the superiority relation and ambiguity
145/// mode. Non-conflicting inputs yield `a`'s polarity. Semantics (Nute / Governatori):
146///  - A `Strict` rule dominates a non-strict opponent.
147///  - Otherwise a side concludes only if its rule is **superior** to the opponent AND can
148///    conclude (a superior `Defeater` merely blocks → `Undecided`, never its own polarity).
149///  - With neither superior, the stand-off is `Undecided` (blocking) or `Ambiguous` (propagating).
150pub fn resolve_conflict(
151    a: &DefeasibleRule,
152    b: &DefeasibleRule,
153    sup: &[(u64, u64)],
154    mode: AmbiguityMode,
155) -> Conclusion {
156    if !rules_conflict(a, b) {
157        return polarity(a);
158    }
159    // Strict indefeasibility.
160    match (a.kind, b.kind) {
161        (RuleKind::Strict, k) if k != RuleKind::Strict => return polarity(a),
162        (k, RuleKind::Strict) if k != RuleKind::Strict => return polarity(b),
163        _ => {}
164    }
165    // Explicit superiority — but a superior Defeater only blocks; it cannot conclude.
166    if is_superior(sup, a.id, b.id) {
167        return if can_conclude(a.kind) {
168            polarity(a)
169        } else {
170            Conclusion::Undecided
171        };
172    }
173    if is_superior(sup, b.id, a.id) {
174        return if can_conclude(b.kind) {
175            polarity(b)
176        } else {
177            Conclusion::Undecided
178        };
179    }
180    // Neither superior → genuine stand-off (an applicable opponent, even a defeater, blocks).
181    match mode {
182        AmbiguityMode::Blocking => Conclusion::Undecided,
183        AmbiguityMode::Propagating => Conclusion::Ambiguous,
184    }
185}
186
187// ─── Integration with the Dung argumentation framework (grounded extension) ─────────
188//
189// Defeasible rules map naturally onto Dung's abstract argumentation: each rule is an argument,
190// conflicting rules attack each other, and the superiority relation orients the attack (the
191// superior rule defeats the inferior; a strict rule is never attacked back). The skeptical
192// GROUNDED extension then resolves which conclusions are justified — and is exactly the
193// ambiguity-BLOCKING reading (an un-oriented conflict leaves both out). Defeaters block but never
194// conclude, so they are excluded from the returned conclusions.
195//
196// NOTE: this bridge intentionally uses the heap-based `argumentation` module; the defeasible
197// *core* above stays zero-heap. It is an additive convenience over `grounded_extension`.
198
199/// Resolve a defeasible rule set against the Dung **grounded extension**: returns the set of rule
200/// ids whose conclusion is justified (skeptically), excluding defeaters. Conflicting rules attack
201/// mutually unless the superiority relation (or strictness) orients the attack one way.
202pub fn grounded_justified_rules(
203    rules: &[DefeasibleRule],
204    sup: &[(u64, u64)],
205) -> std::collections::HashSet<u64> {
206    use crate::modalities::argumentation::{Argument, ArgumentationFramework, Attack, AttackType};
207
208    let mut af = ArgumentationFramework::new();
209    for r in rules {
210        // Encode the conclusion literal+polarity into a NQuin (polarity in the predicate).
211        let concl = NQuin {
212            subject: r.literal,
213            predicate: r.positive as u64,
214            object: 0,
215            context: 0,
216            metadata: 0,
217            parity: 0,
218        };
219        af.add_argument(Argument::new(r.id, String::new(), Vec::new(), concl));
220    }
221    for (i, a) in rules.iter().enumerate() {
222        for b in &rules[i + 1..] {
223            if !rules_conflict(a, b) {
224                continue;
225            }
226            let a_strict = a.kind == RuleKind::Strict;
227            let b_strict = b.kind == RuleKind::Strict;
228            let a_sup = is_superior(sup, a.id, b.id);
229            let b_sup = is_superior(sup, b.id, a.id);
230            // `a` attacks `b` unless `b` is a strict rule `a` is not, or `b` is strictly superior.
231            let a_attacks_b = !(b_strict && !a_strict) && !b_sup;
232            let b_attacks_a = !(a_strict && !b_strict) && !a_sup;
233            if a_attacks_b {
234                af.add_attack(Attack {
235                    attacker: a.id,
236                    target: b.id,
237                    attack_type: AttackType::Rebuttal,
238                    strength: 1.0,
239                });
240            }
241            if b_attacks_a {
242                af.add_attack(Attack {
243                    attacker: b.id,
244                    target: a.id,
245                    attack_type: AttackType::Rebuttal,
246                    strength: 1.0,
247                });
248            }
249        }
250    }
251    af.grounded_extension()
252        .into_iter()
253        .filter(|id| {
254            !rules
255                .iter()
256                .any(|r| r.id == *id && r.kind == RuleKind::Defeater)
257        })
258        .collect()
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::q_hash;
265
266    fn rule(id: u64, kind: RuleKind, lit: u64, positive: bool) -> DefeasibleRule {
267        DefeasibleRule {
268            id,
269            kind,
270            literal: lit,
271            positive,
272        }
273    }
274
275    #[test]
276    fn strict_dominates_and_superiority_decides() {
277        let lit = q_hash("penguin:flies");
278        let r_pos = rule(1, RuleKind::Defeasible, lit, true); // birds fly
279        let r_neg = rule(2, RuleKind::Defeasible, lit, false); // penguins don't
280        assert!(rules_conflict(&r_pos, &r_neg));
281
282        // No superiority, both defeasible → ambiguity (mode-dependent).
283        assert_eq!(
284            resolve_conflict(&r_pos, &r_neg, &[], AmbiguityMode::Blocking),
285            Conclusion::Undecided
286        );
287        assert_eq!(
288            resolve_conflict(&r_pos, &r_neg, &[], AmbiguityMode::Propagating),
289            Conclusion::Ambiguous
290        );
291
292        // "penguins don't fly" is superior → Negative concluded.
293        let sup = [(2u64, 1u64)];
294        assert_eq!(
295            resolve_conflict(&r_pos, &r_neg, &sup, AmbiguityMode::Blocking),
296            Conclusion::Negative
297        );
298
299        // A strict opposing rule dominates regardless of superiority.
300        let r_strict = rule(3, RuleKind::Strict, lit, false);
301        assert_eq!(
302            resolve_conflict(&r_pos, &r_strict, &[], AmbiguityMode::Blocking),
303            Conclusion::Negative
304        );
305    }
306
307    #[test]
308    fn defeater_only_blocks_it_cannot_conclude() {
309        let lit = q_hash("claim:x");
310        let r = rule(1, RuleKind::Defeasible, lit, true);
311        let d = rule(2, RuleKind::Defeater, lit, false);
312        // No superiority: the applicable defeater blocks r; nothing is concluded.
313        assert_eq!(
314            resolve_conflict(&r, &d, &[], AmbiguityMode::Blocking),
315            Conclusion::Undecided
316        );
317        // A SUPERIOR defeater still cannot conclude its own polarity — r is defeated → Undecided.
318        let sup_d = [(2u64, 1u64)];
319        assert_eq!(
320            resolve_conflict(&r, &d, &sup_d, AmbiguityMode::Blocking),
321            Conclusion::Undecided
322        );
323        // When the defeasible rule is superior to the defeater, it concludes.
324        let sup_r = [(1u64, 2u64)];
325        assert_eq!(
326            resolve_conflict(&r, &d, &sup_r, AmbiguityMode::Blocking),
327            Conclusion::Positive
328        );
329    }
330
331    #[test]
332    fn non_conflicting_rules_just_conclude() {
333        let r1 = rule(1, RuleKind::Defeasible, q_hash("a"), true);
334        let r2 = rule(2, RuleKind::Defeasible, q_hash("b"), false);
335        assert!(!rules_conflict(&r1, &r2));
336        assert_eq!(
337            resolve_conflict(&r1, &r2, &[], AmbiguityMode::Blocking),
338            Conclusion::Positive
339        );
340    }
341
342    #[test]
343    fn grounded_extension_resolves_defeasible_conflict() {
344        let flies = q_hash("penguin:flies");
345        let r_bird = rule(1, RuleKind::Defeasible, flies, true); // birds fly
346        let r_peng = rule(2, RuleKind::Defeasible, flies, false); // penguins don't
347
348        // No superiority → mutual attack → grounded extension is skeptical → neither justified.
349        let none = grounded_justified_rules(&[r_bird, r_peng], &[]);
350        assert!(
351            !none.contains(&1) && !none.contains(&2),
352            "un-oriented conflict: neither justified"
353        );
354
355        // "penguins don't fly" superior → only r2 attacks r1 → r2 justified, r1 defeated.
356        let sup = [(2u64, 1u64)];
357        let g = grounded_justified_rules(&[r_bird, r_peng], &sup);
358        assert!(g.contains(&2) && !g.contains(&1));
359
360        // A non-conflicting rule is always justified (no attackers).
361        let r_other = rule(3, RuleKind::Defeasible, q_hash("swims"), true);
362        let g2 = grounded_justified_rules(&[r_bird, r_peng, r_other], &sup);
363        assert!(g2.contains(&3));
364
365        // A defeater blocks but is never itself a justified conclusion.
366        let d = rule(4, RuleKind::Defeater, q_hash("claim"), false);
367        let r_c = rule(5, RuleKind::Defeasible, q_hash("claim"), true);
368        let gd = grounded_justified_rules(&[r_c, d], &[]);
369        assert!(!gd.contains(&4), "defeater never concludes");
370    }
371
372    #[test]
373    fn test_defeasible_evaluation() {
374        let mut out = Vec::with_capacity(10);
375        for _ in 0..10 {
376            out.push(DefeasibleVerdict {
377                claim: NQuin::default(),
378                status: DefeasibleStatus::Strict,
379            });
380        }
381
382        let ctx = q_hash("test_context");
383
384        let mut q_normal = NQuin::default();
385        q_normal.context = ctx;
386        q_normal.predicate = OP_DEFEASIBLE_OVERRIDE as u64;
387
388        let mut q_defeater = NQuin::default();
389        q_defeater.context = ctx;
390        q_defeater.predicate = DEFEATER_BIT | (OP_DEFEASIBLE_OVERRIDE as u64);
391
392        let quins = [q_normal, q_defeater];
393
394        let count = evaluate_defeasible_frame(&quins, ctx, &mut out).unwrap();
395        assert_eq!(count, 2);
396        assert_eq!(out[0].status, DefeasibleStatus::Defeated);
397        assert_eq!(out[1].status, DefeasibleStatus::Overridden);
398    }
399}