qualia_core_db/modalities/argumentation/
mod.rs1use crate::NQuin;
13use std::collections::{HashMap, HashSet};
14
15pub use crate::frame_layout::{ARGUMENT_BIT, ATTACK_BIT, DEFENSE_BIT};
17
18pub mod bipolar;
20pub mod generation;
21pub mod vaf;
22pub use bipolar::BipolarFramework;
23pub use generation::framework_from_trace;
24pub use vaf::ValueArgumentationFramework;
25
26#[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, }
35
36impl Argument {
37 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, }
46 }
47
48 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#[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 Rebuttal,
79 Undercut,
81 Undermine,
83}
84
85#[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 pub fn new() -> Self {
96 Self {
97 arguments: HashMap::new(),
98 attacks: Vec::new(),
99 metadata: 0,
100 }
101 }
102
103 pub fn add_argument(&mut self, argument: Argument) {
105 self.arguments.insert(argument.id, argument);
106 }
107
108 pub fn add_attack(&mut self, attack: Attack) {
110 self.attacks.push(attack);
111 }
112
113 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 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 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 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 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 pub fn preferred_extensions(&self) -> Vec<HashSet<u64>> {
189 let grounded = self.grounded_extension();
191 let mut extensions = vec![grounded.clone()];
192
193 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 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 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 pub fn is_admissible(&self, args: &HashSet<u64>) -> bool {
245 if !self.is_conflict_free(args) {
246 return false;
247 }
248
249 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 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 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 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 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 fn all_ids(&self) -> HashSet<u64> {
318 self.arguments.keys().copied().collect()
319 }
320
321 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 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 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 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 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; }
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 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 pub fn is_complete(&self, args: &HashSet<u64>) -> bool {
399 if !self.is_admissible(args) {
400 return false;
401 }
402 self.all_ids()
404 .iter()
405 .all(|&id| !self.defends(args, id) || args.contains(&id))
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
411pub enum ArgumentStatus {
412 Accepted,
413 Rejected,
414 Undecided,
415}
416
417pub fn framework_to_quins(framework: &ArgumentationFramework, context: u64) -> Vec<NQuin> {
419 let mut quins = Vec::new();
420
421 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 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
452pub fn create_sanctuary_debate() -> ArgumentationFramework {
454 let mut framework = ArgumentationFramework::new();
455
456 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 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 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
499pub const MAX_GROUNDED_ARGS: usize = 128;
501
502pub 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 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 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 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 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 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 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 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 assert!(grounded.contains(&2));
642
643 assert!(!grounded.contains(&1));
645 }
646
647 #[test]
648 fn test_conflict_free() {
649 let framework = create_sanctuary_debate();
650
651 let both_args = HashSet::from([1, 2]);
653 assert!(!framework.is_conflict_free(&both_args));
654
655 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 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 assert_eq!(quins.len(), 3); 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}