Skip to main content

qualia_core_db/modalities/argumentation/
mod.rs

1// Argumentation Frameworks - Dung-style Abstract Argumentation
2// Provides formal debate resolution mechanisms for Peace Infrastructure
3//
4// ⚠ ZERO-HEAP STATUS: this library (and its `vaf`/`bipolar`/`generation` submodules) is
5// HEAP-based by design — `ArgumentationFramework` uses `HashMap`/`HashSet`/`Vec` for dynamic
6// argument/extension sets, and `stable_extensions`/`complete_extensions` return `Vec<HashSet>`.
7// This is the COLD reasoning layer, off the hot path (consistent with AGENTS.md §0 "no
8// Vec/String/Box in HOT PATHS"). The HOT-PATH grounded-extension primitive is the bounded,
9// zero-heap `grounded_contains` (below). A full zero-heap rewrite (bounded bitmask sets, ≤64
10// arguments) is a candidate for the deferred "library-ization" pass.
11
12use crate::NQuin;
13use std::collections::{HashMap, HashSet};
14
15// Canonical bit positions live in the FrameLayout ABI (single source of truth).
16pub use crate::frame_layout::{ARGUMENT_BIT, ATTACK_BIT, DEFENSE_BIT};
17
18// Extensions of the core Dung framework (split per CLAUDE.md §10).
19pub mod bipolar;
20pub mod generation;
21pub mod vaf;
22pub use bipolar::BipolarFramework;
23pub use generation::framework_from_trace;
24pub use vaf::ValueArgumentationFramework;
25
26/// Argument in an abstract argumentation framework
27#[derive(Debug, Clone)]
28pub struct Argument {
29    pub id: u64,
30    pub content: String,
31    pub premise_quins: Vec<NQuin>,
32    pub conclusion_quin: NQuin,
33    pub strength: f32, // Argument strength for weighted argumentation
34}
35
36impl Argument {
37    /// Create a new argument from premises and conclusion
38    pub fn new(id: u64, content: String, premises: Vec<NQuin>, conclusion: NQuin) -> Self {
39        Self {
40            id,
41            content,
42            premise_quins: premises,
43            conclusion_quin: conclusion,
44            strength: 1.0, // Default strength
45        }
46    }
47
48    /// Create an argument with specified strength
49    pub fn with_strength(
50        id: u64,
51        content: String,
52        premises: Vec<NQuin>,
53        conclusion: NQuin,
54        strength: f32,
55    ) -> Self {
56        Self {
57            id,
58            content,
59            premise_quins: premises,
60            conclusion_quin: conclusion,
61            strength,
62        }
63    }
64}
65
66/// Attack relation between arguments
67#[derive(Debug, Clone)]
68pub struct Attack {
69    pub attacker: u64,
70    pub target: u64,
71    pub attack_type: AttackType,
72    pub strength: f32,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum AttackType {
77    /// Direct contradiction of conclusion
78    Rebuttal,
79    /// Attack on premises
80    Undercut,
81    /// Weakening argument strength
82    Undermine,
83}
84
85/// Abstract argumentation framework
86#[derive(Debug, Clone)]
87pub struct ArgumentationFramework {
88    pub arguments: HashMap<u64, Argument>,
89    pub attacks: Vec<Attack>,
90    pub metadata: u64,
91}
92
93impl ArgumentationFramework {
94    /// Create a new empty framework
95    pub fn new() -> Self {
96        Self {
97            arguments: HashMap::new(),
98            attacks: Vec::new(),
99            metadata: 0,
100        }
101    }
102
103    /// Add an argument to the framework
104    pub fn add_argument(&mut self, argument: Argument) {
105        self.arguments.insert(argument.id, argument);
106    }
107
108    /// Add an attack relation
109    pub fn add_attack(&mut self, attack: Attack) {
110        self.attacks.push(attack);
111    }
112
113    /// Get all arguments that attack a given argument
114    pub fn get_attackers(&self, target_id: u64) -> Vec<&Argument> {
115        let mut attackers = Vec::new();
116        for attack in &self.attacks {
117            if attack.target == target_id {
118                if let Some(attacker) = self.arguments.get(&attack.attacker) {
119                    attackers.push(attacker);
120                }
121            }
122        }
123        attackers
124    }
125
126    /// Get all arguments that are attacked by a given argument
127    pub fn get_attacked(&self, attacker_id: u64) -> Vec<&Argument> {
128        let mut attacked = Vec::new();
129        for attack in &self.attacks {
130            if attack.attacker == attacker_id {
131                if let Some(target) = self.arguments.get(&attack.target) {
132                    attacked.push(target);
133                }
134            }
135        }
136        attacked
137    }
138
139    /// Compute grounded extension (unique least fixed point of the characteristic function).
140    ///
141    /// Algorithm (Dung 1995):
142    ///   GE ← ∅
143    ///   Repeat:
144    ///     1. Collect all arguments that are *defeated* by GE (attacked by some member of GE).
145    ///     2. For every remaining argument a not yet in GE:
146    ///        if *every* attacker of a is defeated by GE, add a to GE.
147    ///   Until no change.
148    pub fn grounded_extension(&self) -> HashSet<u64> {
149        let mut grounded: HashSet<u64> = HashSet::new();
150        let mut changed = true;
151
152        while changed {
153            changed = false;
154
155            // Build set of arguments defeated by the current grounded set
156            // (i.e., arguments attacked by at least one member of grounded).
157            let defeated: HashSet<u64> = self
158                .attacks
159                .iter()
160                .filter(|atk| grounded.contains(&atk.attacker))
161                .map(|atk| atk.target)
162                .collect();
163
164            for (&arg_id, _) in &self.arguments {
165                if grounded.contains(&arg_id) {
166                    continue;
167                }
168                // An argument is added to the grounded extension iff all of its
169                // attackers are themselves defeated (i.e., counter-attacked by
170                // something already in grounded).
171                let all_attackers_defeated = self
172                    .attacks
173                    .iter()
174                    .filter(|atk| atk.target == arg_id)
175                    .all(|atk| defeated.contains(&atk.attacker));
176
177                if all_attackers_defeated {
178                    grounded.insert(arg_id);
179                    changed = true;
180                }
181            }
182        }
183
184        grounded
185    }
186
187    /// Compute preferred extensions (maximal conflict-free sets)
188    pub fn preferred_extensions(&self) -> Vec<HashSet<u64>> {
189        // Start with grounded extension as base
190        let grounded = self.grounded_extension();
191        let mut extensions = vec![grounded.clone()];
192
193        // Try to add unattacked arguments iteratively
194        let mut changed = true;
195        while changed {
196            changed = false;
197            let mut new_extensions = Vec::new();
198
199            for extension in &extensions {
200                for (&arg_id, _) in &self.arguments {
201                    if !extension.contains(&arg_id) {
202                        let mut candidate = extension.clone();
203                        candidate.insert(arg_id);
204
205                        if self.is_conflict_free(&candidate) && self.is_admissible(&candidate) {
206                            if !extensions.iter().any(|ext| ext.is_superset(&candidate)) {
207                                new_extensions.push(candidate);
208                                changed = true;
209                            }
210                        }
211                    }
212                }
213            }
214
215            extensions.extend(new_extensions);
216        }
217
218        // Return only maximal extensions
219        let extensions_clone = extensions.clone();
220        extensions
221            .into_iter()
222            .filter(|ext| {
223                !extensions_clone
224                    .iter()
225                    .any(|other| other != ext && other.is_superset(ext))
226            })
227            .collect()
228    }
229
230    /// Check if a set of arguments is conflict-free (no attacks within the set)
231    pub fn is_conflict_free(&self, args: &HashSet<u64>) -> bool {
232        for &arg_id in args {
233            let attacked = self.get_attacked(arg_id);
234            for attacked_arg in attacked {
235                if args.contains(&attacked_arg.id) {
236                    return false;
237                }
238            }
239        }
240        true
241    }
242
243    /// Check if a set of arguments is admissible (conflict-free and defends all its members)
244    pub fn is_admissible(&self, args: &HashSet<u64>) -> bool {
245        if !self.is_conflict_free(args) {
246            return false;
247        }
248
249        // Check if the set defends all its members
250        for &arg_id in args {
251            let attackers = self.get_attackers(arg_id);
252            for attacker in attackers {
253                let is_defended = self
254                    .get_attacked(attacker.id)
255                    .iter()
256                    .any(|defender| args.contains(&defender.id));
257
258                if !is_defended {
259                    return false;
260                }
261            }
262        }
263
264        true
265    }
266
267    /// Compute the argumentation status of an argument
268    pub fn argument_status(&self, arg_id: u64) -> ArgumentStatus {
269        let grounded = self.grounded_extension();
270
271        if grounded.contains(&arg_id) {
272            ArgumentStatus::Accepted
273        } else {
274            let preferred = self.preferred_extensions();
275            let accepted_in_all = preferred.iter().all(|ext| ext.contains(&arg_id));
276            let accepted_in_some = preferred.iter().any(|ext| ext.contains(&arg_id));
277
278            if accepted_in_all {
279                ArgumentStatus::Accepted
280            } else if accepted_in_some {
281                ArgumentStatus::Undecided
282            } else {
283                ArgumentStatus::Rejected
284            }
285        }
286    }
287
288    /// Resolve a debate using skeptical reasoning (intersection of all preferred extensions)
289    pub fn resolve_skeptically(&self) -> HashSet<u64> {
290        let preferred = self.preferred_extensions();
291        if preferred.is_empty() {
292            return HashSet::new();
293        }
294
295        // Return intersection of all preferred extensions
296        let mut result = preferred[0].clone();
297        for extension in &preferred[1..] {
298            result = result.intersection(extension).cloned().collect();
299        }
300
301        result
302    }
303
304    /// Resolve a debate using credulous reasoning (union of all preferred extensions)
305    pub fn resolve_credulously(&self) -> HashSet<u64> {
306        let preferred = self.preferred_extensions();
307        let mut result = HashSet::new();
308
309        for extension in preferred {
310            result.extend(extension);
311        }
312
313        result
314    }
315
316    /// The set of all argument ids in the framework.
317    fn all_ids(&self) -> HashSet<u64> {
318        self.arguments.keys().copied().collect()
319    }
320
321    /// Does `args` attack `target` (some member of `args` attacks it)?
322    fn set_attacks(&self, args: &HashSet<u64>, target: u64) -> bool {
323        self.attacks
324            .iter()
325            .any(|atk| atk.target == target && args.contains(&atk.attacker))
326    }
327
328    /// **Stable extensions** (Dung): a conflict-free set that attacks *every* argument outside it.
329    /// Computed by testing each conflict-free subset (exponential — bounded by frame size, as
330    /// abstract frameworks here are small). Every stable extension is also preferred.
331    pub fn stable_extensions(&self) -> Vec<HashSet<u64>> {
332        let ids: Vec<u64> = self.arguments.keys().copied().collect();
333        let n = ids.len();
334        let mut out = Vec::new();
335        if n > 20 {
336            // Guard against blow-up; fall back to preferred extensions that are also stable.
337            for ext in self.preferred_extensions() {
338                if self.is_stable(&ext) {
339                    out.push(ext);
340                }
341            }
342            return out;
343        }
344        for mask in 0u32..(1u32 << n) {
345            let set: HashSet<u64> = (0..n)
346                .filter(|&i| (mask >> i) & 1 == 1)
347                .map(|i| ids[i])
348                .collect();
349            if self.is_conflict_free(&set) && self.is_stable(&set) {
350                out.push(set);
351            }
352        }
353        out
354    }
355
356    /// Is `args` a **stable** extension: conflict-free and attacks every argument not in it?
357    pub fn is_stable(&self, args: &HashSet<u64>) -> bool {
358        if !self.is_conflict_free(args) {
359            return false;
360        }
361        self.all_ids()
362            .iter()
363            .filter(|id| !args.contains(id))
364            .all(|&outside| self.set_attacks(args, outside))
365    }
366
367    /// **Complete extensions** (Dung): an admissible set that contains *every* argument it
368    /// defends (its own fixed point under the characteristic function). The grounded extension is
369    /// the least complete extension; each preferred extension is a maximal complete one.
370    pub fn complete_extensions(&self) -> Vec<HashSet<u64>> {
371        let ids: Vec<u64> = self.arguments.keys().copied().collect();
372        let n = ids.len();
373        let mut out = Vec::new();
374        if n > 20 {
375            return out; // bounded; abstract frameworks here are small
376        }
377        for mask in 0u32..(1u32 << n) {
378            let set: HashSet<u64> = (0..n)
379                .filter(|&i| (mask >> i) & 1 == 1)
380                .map(|i| ids[i])
381                .collect();
382            if self.is_complete(&set) {
383                out.push(set);
384            }
385        }
386        out
387    }
388
389    /// Does `args` **defend** `arg` (every attacker of `arg` is attacked by `args`)?
390    pub fn defends(&self, args: &HashSet<u64>, arg: u64) -> bool {
391        self.attacks
392            .iter()
393            .filter(|atk| atk.target == arg)
394            .all(|atk| self.set_attacks(args, atk.attacker))
395    }
396
397    /// Is `args` a **complete** extension: admissible and contains every argument it defends?
398    pub fn is_complete(&self, args: &HashSet<u64>) -> bool {
399        if !self.is_admissible(args) {
400            return false;
401        }
402        // Every argument the set defends must already be in the set.
403        self.all_ids()
404            .iter()
405            .all(|&id| !self.defends(args, id) || args.contains(&id))
406    }
407}
408
409/// Argument status in the framework
410#[derive(Debug, Clone, PartialEq, Eq)]
411pub enum ArgumentStatus {
412    Accepted,
413    Rejected,
414    Undecided,
415}
416
417/// Convert argument framework to NQuin representation for storage
418pub fn framework_to_quins(framework: &ArgumentationFramework, context: u64) -> Vec<NQuin> {
419    let mut quins = Vec::new();
420
421    // Store arguments
422    for (arg_id, argument) in &framework.arguments {
423        let mut quin = NQuin {
424            subject: *arg_id,
425            predicate: crate::q_hash("has_argument"),
426            object: crate::q_hash(&argument.content),
427            context,
428            metadata: ARGUMENT_BIT | ((argument.strength as u64) << 32),
429            parity: 0,
430        };
431        quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
432        quins.push(quin);
433    }
434
435    // Store attacks
436    for attack in &framework.attacks {
437        let mut quin = NQuin {
438            subject: attack.attacker,
439            predicate: crate::q_hash("attacks"),
440            object: attack.target,
441            context,
442            metadata: ATTACK_BIT | ((attack.strength as u64) << 32),
443            parity: 0,
444        };
445        quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
446        quins.push(quin);
447    }
448
449    quins
450}
451
452/// Create a simple debate about sanctuary boundaries
453pub fn create_sanctuary_debate() -> ArgumentationFramework {
454    let mut framework = ArgumentationFramework::new();
455
456    // Argument 1: Sanctuary should protect all life
457    let arg1 = Argument::new(
458        1,
459        "Sanctuary must protect all living beings".to_string(),
460        vec![],
461        NQuin {
462            subject: crate::q_hash("sanctuary"),
463            predicate: crate::q_hash("protects"),
464            object: crate::q_hash("all_life"),
465            context: 100,
466            metadata: 0,
467            parity: 0,
468        },
469    );
470    framework.add_argument(arg1);
471
472    // Argument 2: Resource constraints limit protection scope
473    let arg2 = Argument::new(
474        2,
475        "Limited resources require prioritized protection".to_string(),
476        vec![],
477        NQuin {
478            subject: crate::q_hash("sanctuary"),
479            predicate: crate::q_hash("protects"),
480            object: crate::q_hash("prioritized_life"),
481            context: 101,
482            metadata: 0,
483            parity: 0,
484        },
485    );
486    framework.add_argument(arg2);
487
488    // Argument 2 attacks Argument 1 (undercut)
489    framework.add_attack(Attack {
490        attacker: 2,
491        target: 1,
492        attack_type: AttackType::Undercut,
493        strength: 0.8,
494    });
495
496    framework
497}
498
499/// Max arguments considered by the zero-heap grounded-extension membership test.
500pub const MAX_GROUNDED_ARGS: usize = 128;
501
502/// Zero-heap Dung (1995) grounded-extension membership test over caller-supplied,
503/// bounded argument and attack arrays. Returns whether `goal` is justified (in the
504/// grounded extension). No allocation — fixed stack buffers only; suitable for the
505/// Webizen VM hot path. (`ArgumentationFramework::grounded_extension` is the
506/// heap-using batch variant for the cold path.)
507pub fn grounded_contains(args: &[u64], attacks: &[(u64, u64)], goal: u64) -> bool {
508    let n = args.len().min(MAX_GROUNDED_ARGS);
509    let mut grounded = [false; MAX_GROUNDED_ARGS];
510    let mut defeated = [false; MAX_GROUNDED_ARGS];
511
512    let index_of = |id: u64| -> Option<usize> { args[..n].iter().position(|&a| a == id) };
513
514    loop {
515        // defeated = arguments attacked by a current grounded member.
516        for d in defeated.iter_mut().take(n) {
517            *d = false;
518        }
519        for &(attacker, target) in attacks {
520            if let Some(ai) = index_of(attacker) {
521                if grounded[ai] {
522                    if let Some(ti) = index_of(target) {
523                        defeated[ti] = true;
524                    }
525                }
526            }
527        }
528
529        let mut changed = false;
530        for i in 0..n {
531            if grounded[i] {
532                continue;
533            }
534            // args[i] joins the grounded set iff every attacker of it is defeated.
535            let mut all_attackers_defeated = true;
536            for &(attacker, target) in attacks {
537                if target == args[i] {
538                    match index_of(attacker) {
539                        Some(ai) if defeated[ai] => {}
540                        _ => {
541                            all_attackers_defeated = false;
542                            break;
543                        }
544                    }
545                }
546            }
547            if all_attackers_defeated {
548                grounded[i] = true;
549                changed = true;
550            }
551        }
552        if !changed {
553            break;
554        }
555    }
556
557    match index_of(goal) {
558        Some(gi) => grounded[gi],
559        None => false,
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566
567    #[test]
568    fn grounded_contains_zero_heap_matches_dung() {
569        // A unattacked; A attacks B; B attacks C. Grounded = {A, C}.
570        let args = [1u64, 2, 3];
571        let attacks = [(1u64, 2u64), (2, 3)];
572        assert!(grounded_contains(&args, &attacks, 1), "A is justified");
573        assert!(!grounded_contains(&args, &attacks, 2), "B is defeated by A");
574        assert!(
575            grounded_contains(&args, &attacks, 3),
576            "C is reinstated (its attacker B is defeated)"
577        );
578        assert!(
579            !grounded_contains(&args, &attacks, 99),
580            "unknown argument is not justified"
581        );
582    }
583
584    #[test]
585    fn stable_and_complete_extensions() {
586        let mk_arg = |id| Argument::new(id, String::new(), Vec::new(), NQuin::default());
587        let mk_atk = |a, b| Attack {
588            attacker: a,
589            target: b,
590            attack_type: AttackType::Rebuttal,
591            strength: 1.0,
592        };
593
594        // 2-cycle 1 ↔ 2.
595        let mut af = ArgumentationFramework::new();
596        af.add_argument(mk_arg(1));
597        af.add_argument(mk_arg(2));
598        af.add_attack(mk_atk(1, 2));
599        af.add_attack(mk_atk(2, 1));
600
601        // Stable extensions: {1} and {2}.
602        let stable = af.stable_extensions();
603        assert_eq!(stable.len(), 2);
604        assert!(stable.iter().any(|e| *e == HashSet::from([1u64])));
605        assert!(stable.iter().any(|e| *e == HashSet::from([2u64])));
606        assert!(af.is_stable(&HashSet::from([1u64])));
607        assert!(
608            !af.is_stable(&HashSet::new()),
609            "{{}} attacks nothing outside → not stable"
610        );
611
612        // Complete extensions: {}, {1}, {2} (grounded {} is the least complete).
613        let complete = af.complete_extensions();
614        assert_eq!(complete.len(), 3);
615        assert!(complete.iter().any(|e| e.is_empty()));
616        assert!(af.is_complete(&HashSet::new()));
617        assert!(af.is_complete(&HashSet::from([1u64])));
618
619        // Reinstatement chain 1→2→3: {1,3} is the unique stable extension and is complete.
620        let mut chain = ArgumentationFramework::new();
621        for i in 1u64..=3 {
622            chain.add_argument(mk_arg(i));
623        }
624        chain.add_attack(mk_atk(1, 2));
625        chain.add_attack(mk_atk(2, 3));
626        assert!(chain.is_stable(&HashSet::from([1u64, 3])));
627        assert!(chain.is_complete(&HashSet::from([1u64, 3])));
628        assert!(
629            chain.defends(&HashSet::from([1u64]), 3),
630            "1 defends 3 by attacking 2"
631        );
632        assert_eq!(chain.stable_extensions(), vec![HashSet::from([1u64, 3])]);
633    }
634
635    #[test]
636    fn test_grounded_extension() {
637        let framework = create_sanctuary_debate();
638        let grounded = framework.grounded_extension();
639
640        // Argument 2 should be in grounded extension (unattacked)
641        assert!(grounded.contains(&2));
642
643        // Argument 1 should not be (attacked by 2)
644        assert!(!grounded.contains(&1));
645    }
646
647    #[test]
648    fn test_conflict_free() {
649        let framework = create_sanctuary_debate();
650
651        // Set with both arguments should not be conflict-free
652        let both_args = HashSet::from([1, 2]);
653        assert!(!framework.is_conflict_free(&both_args));
654
655        // Set with only argument 2 should be conflict-free
656        let only_arg2 = HashSet::from([2]);
657        assert!(framework.is_conflict_free(&only_arg2));
658    }
659
660    #[test]
661    fn test_argument_status() {
662        let framework = create_sanctuary_debate();
663
664        assert_eq!(framework.argument_status(2), ArgumentStatus::Accepted);
665        assert_eq!(framework.argument_status(1), ArgumentStatus::Rejected);
666    }
667
668    #[test]
669    fn test_skeptical_resolution() {
670        let framework = create_sanctuary_debate();
671        let skeptical = framework.resolve_skeptically();
672
673        // Should only include arguments accepted in all preferred extensions
674        assert!(skeptical.contains(&2));
675        assert!(!skeptical.contains(&1));
676    }
677
678    #[test]
679    fn test_framework_to_quins() {
680        let framework = create_sanctuary_debate();
681        let quins = framework_to_quins(&framework, 123);
682
683        // Should have quins for arguments and attacks
684        assert_eq!(quins.len(), 3); // 2 arguments + 1 attack
685
686        // Check metadata bits
687        let arg_quin = quins
688            .iter()
689            .find(|q| q.predicate == crate::q_hash("has_argument"))
690            .unwrap();
691        assert!(arg_quin.metadata & ARGUMENT_BIT != 0);
692
693        let attack_quin = quins
694            .iter()
695            .find(|q| q.predicate == crate::q_hash("attacks"))
696            .unwrap();
697        assert!(attack_quin.metadata & ATTACK_BIT != 0);
698    }
699}