Skip to main content

qualia_core_db/modalities/argumentation/
generation.rs

1//! Dynamic argument-generation engine — build a Dung framework directly from raw deontic / LTL
2//! trace verdicts. Each verdict becomes an argument concluding a signed literal; verdicts with
3//! **complementary** conclusions (same literal, opposite polarity) attack each other. This is the
4//! bridge from the engine's modal/deontic traces into abstract argumentation, so a conflict in
5//! the trace becomes a resolvable debate.
6
7use super::{Argument, ArgumentationFramework, Attack, AttackType};
8use crate::NQuin;
9
10/// Build a framework from trace `entries` `(arg_id, conclusion_literal, positive)`: each becomes
11/// an argument; any two with the same `conclusion_literal` and opposite `positive` mutually
12/// attack (a rebuttal). The conclusion is encoded into the argument's `conclusion_quin`
13/// (`subject = literal`, `predicate = polarity`).
14pub fn framework_from_trace(entries: &[(u64, u64, bool)]) -> ArgumentationFramework {
15    let mut af = ArgumentationFramework::new();
16    for &(id, lit, positive) in entries {
17        let mut concl = NQuin {
18            subject: lit,
19            predicate: positive as u64,
20            object: 0,
21            context: 0,
22            metadata: 0,
23            parity: 0,
24        };
25        concl.parity = concl.subject ^ concl.predicate ^ concl.object ^ concl.context;
26        af.add_argument(Argument::new(id, String::new(), Vec::new(), concl));
27    }
28    for (i, &(id_a, lit_a, pos_a)) in entries.iter().enumerate() {
29        for &(id_b, lit_b, pos_b) in &entries[i + 1..] {
30            if lit_a == lit_b && pos_a != pos_b {
31                af.add_attack(Attack {
32                    attacker: id_a,
33                    target: id_b,
34                    attack_type: AttackType::Rebuttal,
35                    strength: 1.0,
36                });
37                af.add_attack(Attack {
38                    attacker: id_b,
39                    target: id_a,
40                    attack_type: AttackType::Rebuttal,
41                    strength: 1.0,
42                });
43            }
44        }
45    }
46    af
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn complementary_verdicts_become_a_debate() {
55        let permit = crate::q_hash("act:disclose");
56        // Trace: arg 1 concludes (disclose, +); arg 2 concludes (disclose, −); arg 3 unrelated.
57        let af = framework_from_trace(&[
58            (1, permit, true),
59            (2, permit, false),
60            (3, crate::q_hash("act:other"), true),
61        ]);
62        assert_eq!(af.arguments.len(), 3);
63        // 1 and 2 conflict (mutual attack); 3 is independent.
64        assert!(af.attacks.iter().any(|a| a.attacker == 1 && a.target == 2));
65        assert!(af.attacks.iter().any(|a| a.attacker == 2 && a.target == 1));
66        let g = af.grounded_extension();
67        assert!(
68            !g.contains(&1) && !g.contains(&2),
69            "the conflict is undecided in the grounded extension"
70        );
71        assert!(g.contains(&3), "the independent verdict stands");
72    }
73
74    #[test]
75    fn agreeing_verdicts_do_not_attack() {
76        let lit = crate::q_hash("act:x");
77        // Two arguments concluding the SAME polarity do not conflict.
78        let af = framework_from_trace(&[(1, lit, true), (2, lit, true)]);
79        assert!(af.attacks.is_empty());
80        let g = af.grounded_extension();
81        assert!(g.contains(&1) && g.contains(&2));
82    }
83}