1use super::argument::{Argument, ArgumentId, Origin, construct_arguments};
8use super::attacks::{Attack, AttackKind, compute_attacks};
9use super::kb::KnowledgeBase;
10use super::language::Literal;
11use super::rules::{Rule, RuleId};
12use crate::framework::ArgumentationFramework;
13use std::collections::HashSet;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
28pub enum DefeatOrdering {
29 #[default]
31 LastLink,
32 WeakestLink,
34}
35
36#[derive(Debug, Default)]
38pub struct StructuredSystem {
39 kb: KnowledgeBase,
40 rules: Vec<Rule>,
41 preferences: Vec<(RuleId, RuleId)>,
44 premise_preferences: Vec<(Literal, Literal)>,
49 ordering: DefeatOrdering,
51 next_rule_id: usize,
52}
53
54#[derive(Debug)]
59pub struct BuildOutput {
60 pub arguments: Vec<Argument>,
62 pub attacks: Vec<Attack>,
64 pub framework: ArgumentationFramework<ArgumentId>,
66 pub rules: Vec<Rule>,
69}
70
71impl BuildOutput {
72 pub fn conclusions_in(&self, extension: &HashSet<ArgumentId>) -> HashSet<&Literal> {
77 self.arguments
78 .iter()
79 .filter(|a| extension.contains(&a.id))
80 .map(|a| &a.conclusion)
81 .collect()
82 }
83
84 pub fn argument_by_conclusion(&self, literal: &Literal) -> Option<&Argument> {
90 self.arguments.iter().find(|a| &a.conclusion == literal)
91 }
92
93 pub fn arguments_with_conclusion(&self, literal: &Literal) -> Vec<&Argument> {
95 self.arguments
96 .iter()
97 .filter(|a| &a.conclusion == literal)
98 .collect()
99 }
100
101 pub fn check_postulates(
109 &self,
110 extension: &HashSet<ArgumentId>,
111 ) -> super::postulates::PostulateReport {
112 super::postulates::check_postulates(&self.arguments, &self.rules, extension)
113 }
114}
115
116impl StructuredSystem {
117 pub fn new() -> Self {
119 Self::default()
120 }
121
122 pub fn with_ordering(ordering: DefeatOrdering) -> Self {
124 Self {
125 ordering,
126 ..Self::default()
127 }
128 }
129
130 pub fn ordering(&self) -> DefeatOrdering {
132 self.ordering
133 }
134
135 pub fn kb_mut(&mut self) -> &mut KnowledgeBase {
137 &mut self.kb
138 }
139
140 pub fn add_necessary(&mut self, l: Literal) {
142 self.kb.add_necessary(l);
143 }
144
145 pub fn add_ordinary(&mut self, l: Literal) {
147 self.kb.add_ordinary(l);
148 }
149
150 pub fn add_strict_rule(&mut self, premises: Vec<Literal>, conclusion: Literal) -> RuleId {
152 let id = RuleId(self.next_rule_id);
153 self.next_rule_id += 1;
154 self.rules.push(Rule::strict(id, premises, conclusion));
155 id
156 }
157
158 pub fn add_defeasible_rule(&mut self, premises: Vec<Literal>, conclusion: Literal) -> RuleId {
160 let id = RuleId(self.next_rule_id);
161 self.next_rule_id += 1;
162 self.rules.push(Rule::defeasible(id, premises, conclusion));
163 id
164 }
165
166 pub fn add_undercut_rule(&mut self, target: RuleId, premises: Vec<Literal>) -> RuleId {
174 let conclusion = Literal::undercut_marker(target.0);
175 self.add_defeasible_rule(premises, conclusion)
176 }
177
178 pub fn prefer_rule(
192 &mut self,
193 preferred: RuleId,
194 less_preferred: RuleId,
195 ) -> Result<(), crate::Error> {
196 if preferred == less_preferred {
197 return Err(crate::Error::Aspic(format!(
198 "reflexive preference rejected: rule {:?} cannot be preferred to itself",
199 preferred
200 )));
201 }
202 if self.is_preferred(less_preferred, preferred) {
203 return Err(crate::Error::Aspic(format!(
204 "cyclic preference rejected: rule {:?} is already (transitively) preferred to {:?}",
205 less_preferred, preferred
206 )));
207 }
208 self.preferences.push((preferred, less_preferred));
209 Ok(())
210 }
211
212 pub fn prefer_premise(
224 &mut self,
225 preferred: Literal,
226 less_preferred: Literal,
227 ) -> Result<(), crate::Error> {
228 if preferred == less_preferred {
229 return Err(crate::Error::Aspic(format!(
230 "reflexive premise preference rejected: {:?} cannot be preferred to itself",
231 preferred
232 )));
233 }
234 if self.is_premise_preferred(&less_preferred, &preferred) {
235 return Err(crate::Error::Aspic(format!(
236 "cyclic premise preference rejected: {:?} is already (transitively) preferred to {:?}",
237 less_preferred, preferred
238 )));
239 }
240 self.premise_preferences.push((preferred, less_preferred));
241 Ok(())
242 }
243
244 pub fn is_premise_preferred(&self, a: &Literal, b: &Literal) -> bool {
247 if a == b {
248 return false;
249 }
250 let mut visited: HashSet<&Literal> = HashSet::new();
251 let mut frontier: Vec<&Literal> = vec![a];
252 while let Some(current) = frontier.pop() {
253 if !visited.insert(current) {
254 continue;
255 }
256 for (p, lp) in &self.premise_preferences {
257 if p == current {
258 if lp == b {
259 return true;
260 }
261 frontier.push(lp);
262 }
263 }
264 }
265 false
266 }
267
268 pub fn premise_preferences(&self) -> &[(Literal, Literal)] {
270 &self.premise_preferences
271 }
272
273 pub fn kb(&self) -> &KnowledgeBase {
275 &self.kb
276 }
277
278 pub fn rules(&self) -> &[Rule] {
280 &self.rules
281 }
282
283 pub fn preferences(&self) -> &[(RuleId, RuleId)] {
285 &self.preferences
286 }
287
288 fn is_preferred(&self, a: RuleId, b: RuleId) -> bool {
291 if a == b {
293 return false;
294 }
295 let mut visited: HashSet<RuleId> = HashSet::new();
297 let mut frontier: Vec<RuleId> = vec![a];
298 while let Some(current) = frontier.pop() {
299 if !visited.insert(current) {
300 continue;
301 }
302 for (p, lp) in &self.preferences {
303 if *p == current {
304 if *lp == b {
305 return true;
306 }
307 frontier.push(*lp);
308 }
309 }
310 }
311 false
312 }
313
314 fn last_defeasible_frontier(&self, arg: &Argument, args: &[Argument]) -> Vec<RuleId> {
324 match &arg.origin {
325 Origin::Premise(_) => Vec::new(),
326 Origin::RuleApplication(rid) => {
327 if let Some(rule) = self.rules.iter().find(|r| r.id == *rid)
328 && rule.is_defeasible()
329 {
330 return vec![*rid];
331 }
332 let mut result = Vec::new();
334 for sub_id in &arg.sub_arguments {
335 if let Some(sub) = args.iter().find(|a| a.id == *sub_id) {
336 result.extend(self.last_defeasible_frontier(sub, args));
337 }
338 }
339 result
340 }
341 }
342 }
343
344 fn all_defeasible_rules(&self, arg: &Argument, args: &[Argument]) -> Vec<RuleId> {
348 let mut out = Vec::new();
349 if let Origin::RuleApplication(rid) = &arg.origin
350 && let Some(rule) = self.rules.iter().find(|r| r.id == *rid)
351 && rule.is_defeasible()
352 {
353 out.push(*rid);
354 }
355 for sub_id in &arg.sub_arguments {
356 if let Some(sub) = args.iter().find(|a| a.id == *sub_id) {
357 out.extend(self.all_defeasible_rules(sub, args));
358 }
359 }
360 out
361 }
362
363 fn all_ordinary_premises(&self, arg: &Argument, args: &[Argument]) -> Vec<Literal> {
366 let mut out = Vec::new();
367 match &arg.origin {
368 Origin::Premise(p) => {
369 if p.is_defeasible() {
370 out.push(p.literal().clone());
371 }
372 }
373 Origin::RuleApplication(_) => {
374 for sub_id in &arg.sub_arguments {
375 if let Some(sub) = args.iter().find(|a| a.id == *sub_id) {
376 out.extend(self.all_ordinary_premises(sub, args));
377 }
378 }
379 }
380 }
381 out
382 }
383
384 fn last_premise_frontier(&self, arg: &Argument, args: &[Argument]) -> Vec<Literal> {
393 match &arg.origin {
394 Origin::Premise(p) => {
395 if p.is_defeasible() {
396 vec![p.literal().clone()]
397 } else {
398 Vec::new()
399 }
400 }
401 Origin::RuleApplication(_) => {
402 let mut out = Vec::new();
403 for sub_id in &arg.sub_arguments {
404 if let Some(sub) = args.iter().find(|a| a.id == *sub_id) {
405 out.extend(self.last_premise_frontier(sub, args));
406 }
407 }
408 out
409 }
410 }
411 }
412
413 fn is_defeat(&self, attack: &Attack, args: &[Argument]) -> bool {
426 if attack.kind == AttackKind::Undercut {
427 return true;
428 }
429 let attacker = args
430 .iter()
431 .find(|a| a.id == attack.attacker)
432 .expect("attack references nonexistent attacker argument");
433 let target = args
434 .iter()
435 .find(|a| a.id == attack.target)
436 .expect("attack references nonexistent target argument");
437
438 let attacker_strictly_less = match self.ordering {
442 DefeatOrdering::LastLink => self.last_link_prec(attacker, target, args),
443 DefeatOrdering::WeakestLink => self.weakest_link_prec(attacker, target, args),
444 };
445 !attacker_strictly_less
446 }
447
448 fn rule_set_strict_lt(&self, gamma: &[RuleId], gamma_prime: &[RuleId]) -> bool {
467 if gamma.is_empty() {
468 return false;
469 }
470 if gamma_prime.is_empty() {
471 return true;
472 }
473 gamma
474 .iter()
475 .any(|x| gamma_prime.iter().all(|y| self.is_preferred(*y, *x)))
476 }
477
478 fn premise_set_strict_lt(&self, gamma: &[Literal], gamma_prime: &[Literal]) -> bool {
480 if gamma.is_empty() {
481 return false;
482 }
483 if gamma_prime.is_empty() {
484 return true;
485 }
486 gamma
487 .iter()
488 .any(|x| gamma_prime.iter().all(|y| self.is_premise_preferred(y, x)))
489 }
490
491 fn rule_set_lte(&self, gamma: &[RuleId], gamma_prime: &[RuleId]) -> bool {
499 if gamma.is_empty() {
500 return false;
501 }
502 if gamma_prime.is_empty() {
503 return true;
504 }
505 gamma
506 .iter()
507 .any(|x| gamma_prime.iter().all(|y| !self.is_preferred(*x, *y)))
508 }
509
510 fn premise_set_lte(&self, gamma: &[Literal], gamma_prime: &[Literal]) -> bool {
512 if gamma.is_empty() {
513 return false;
514 }
515 if gamma_prime.is_empty() {
516 return true;
517 }
518 gamma
519 .iter()
520 .any(|x| gamma_prime.iter().all(|y| !self.is_premise_preferred(x, y)))
521 }
522
523 fn last_link_prec(&self, a: &Argument, b: &Argument, args: &[Argument]) -> bool {
529 let a_rules = self.last_defeasible_frontier(a, args);
530 let b_rules = self.last_defeasible_frontier(b, args);
531 if self.rule_set_strict_lt(&a_rules, &b_rules) {
532 return true;
533 }
534 if a_rules.is_empty() && b_rules.is_empty() {
535 let a_prems = self.last_premise_frontier(a, args);
536 let b_prems = self.last_premise_frontier(b, args);
537 return self.premise_set_strict_lt(&a_prems, &b_prems);
538 }
539 false
540 }
541
542 fn weakest_link_prec(&self, a: &Argument, b: &Argument, args: &[Argument]) -> bool {
564 let a_rules = self.all_defeasible_rules(a, args);
565 let b_rules = self.all_defeasible_rules(b, args);
566 let a_prems = self.all_ordinary_premises(a, args);
567 let b_prems = self.all_ordinary_premises(b, args);
568
569 let both_strict_arg = a_rules.is_empty() && b_rules.is_empty();
570 let both_firm = a_prems.is_empty() && b_prems.is_empty();
571
572 if both_strict_arg {
573 return self.premise_set_strict_lt(&a_prems, &b_prems);
574 }
575 if both_firm {
576 return self.rule_set_strict_lt(&a_rules, &b_rules);
577 }
578
579 let prems_lte = self.premise_set_lte(&a_prems, &b_prems);
580 let rules_lte = self.rule_set_lte(&a_rules, &b_rules);
581 let prems_lt = self.premise_set_strict_lt(&a_prems, &b_prems);
582 let rules_lt = self.rule_set_strict_lt(&a_rules, &b_rules);
583 prems_lte && rules_lte && (prems_lt || rules_lt)
584 }
585
586 #[must_use = "build_framework returns a Result whose Ok carries the constructed framework, attacks, and arguments — discarding it is almost always a bug"]
591 pub fn build_framework(&self) -> Result<BuildOutput, crate::Error> {
592 let arguments = construct_arguments(&self.kb, &self.rules)?;
593 let attacks = compute_attacks(&arguments, &self.rules);
594 let mut framework = ArgumentationFramework::new();
595 for arg in &arguments {
596 framework.add_argument(arg.id);
597 }
598 for attack in &attacks {
599 if self.is_defeat(attack, &arguments) {
600 framework.add_attack(&attack.attacker, &attack.target)?;
601 }
602 }
603 Ok(BuildOutput {
604 arguments,
605 attacks,
606 framework,
607 rules: self.rules.clone(),
608 })
609 }
610
611 #[must_use = "to_framework returns the constructed Dung AF — discarding it leaves no observable effect"]
617 pub fn to_framework(&self) -> Result<ArgumentationFramework<ArgumentId>, crate::Error> {
618 Ok(self.build_framework()?.framework)
619 }
620
621 #[must_use = "arguments() returns the constructed argument list — discarding it leaves no observable effect"]
624 pub fn arguments(&self) -> Result<Vec<Argument>, crate::Error> {
625 Ok(self.build_framework()?.arguments)
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632 use crate::aspic::language::Literal;
633
634 #[test]
635 fn penguin_example_resolves_correctly() {
636 let mut system = StructuredSystem::new();
637 system.kb_mut().add_ordinary(Literal::atom("penguin"));
638 system.add_strict_rule(vec![Literal::atom("penguin")], Literal::atom("bird"));
639 let r1 = system.add_defeasible_rule(vec![Literal::atom("bird")], Literal::atom("flies"));
640 let r2 = system.add_defeasible_rule(vec![Literal::atom("penguin")], Literal::neg("flies"));
641 system.prefer_rule(r2, r1).unwrap();
642
643 let built = system.build_framework().unwrap();
644 let preferred = built.framework.preferred_extensions().unwrap();
645 assert_eq!(preferred.len(), 1);
646 let ext = &preferred[0];
647 let flies_arg = built
648 .arguments
649 .iter()
650 .find(|a| a.conclusion == Literal::atom("flies"));
651 let not_flies_arg = built
652 .arguments
653 .iter()
654 .find(|a| a.conclusion == Literal::neg("flies"));
655 if let (Some(f), Some(nf)) = (flies_arg, not_flies_arg) {
656 assert!(!ext.contains(&f.id));
657 assert!(ext.contains(&nf.id));
658 }
659 }
660
661 #[test]
662 fn transitive_preferences_are_respected() {
663 let mut system = StructuredSystem::new();
665 system.kb_mut().add_ordinary(Literal::atom("p"));
666 system.kb_mut().add_ordinary(Literal::atom("q"));
667 system.kb_mut().add_ordinary(Literal::atom("r"));
668 let r1 = system.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("x"));
669 let r2 = system.add_defeasible_rule(vec![Literal::atom("q")], Literal::atom("y"));
670 let r3 = system.add_defeasible_rule(vec![Literal::atom("r")], Literal::atom("z"));
671 system.prefer_rule(r3, r2).unwrap();
672 system.prefer_rule(r2, r1).unwrap();
673 assert!(
674 system.is_preferred(r3, r1),
675 "transitive r3 > r1 should hold"
676 );
677 }
678
679 #[test]
680 fn undercut_helper_constructs_reserved_literal() {
681 let mut system = StructuredSystem::new();
682 system.kb_mut().add_ordinary(Literal::atom("p"));
683 let target = system.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("q"));
684 system.kb_mut().add_ordinary(Literal::atom("trigger"));
685 let uc = system.add_undercut_rule(target, vec![Literal::atom("trigger")]);
686 let uc_rule = system.rules().iter().find(|r| r.id == uc).unwrap();
687 assert_eq!(uc_rule.conclusion, Literal::undercut_marker(target.0));
688 }
689
690 #[test]
691 fn read_side_getters_expose_state() {
692 let mut system = StructuredSystem::new();
693 system.kb_mut().add_ordinary(Literal::atom("p"));
694 let r1 = system.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("q"));
695 let r2 = system.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("r"));
696 assert_eq!(system.kb().premises().len(), 1);
697 assert_eq!(system.rules().len(), 2);
698 assert!(system.preferences().is_empty());
699 system.prefer_rule(r1, r2).unwrap();
700 assert_eq!(system.preferences().len(), 1);
701 }
702
703 #[test]
704 fn empty_system_produces_empty_framework() {
705 let system = StructuredSystem::new();
706 let af = system.to_framework().unwrap();
707 assert_eq!(af.arguments().count(), 0);
708 }
709
710 #[test]
711 fn weakest_link_case3_succeeds_with_one_strict_dimension() {
712 let mut system = StructuredSystem::with_ordering(DefeatOrdering::WeakestLink);
728 system.kb_mut().add_ordinary(Literal::atom("p_lo"));
729 system.kb_mut().add_ordinary(Literal::atom("p_hi"));
730 system
731 .prefer_premise(Literal::atom("p_hi"), Literal::atom("p_lo"))
732 .unwrap();
733 let _r_a = system.add_defeasible_rule(vec![Literal::atom("p_lo")], Literal::atom("q"));
734 let _r_b = system.add_defeasible_rule(vec![Literal::atom("p_hi")], Literal::neg("q"));
735
736 let built = system.build_framework().unwrap();
737 let q_arg = built
738 .arguments
739 .iter()
740 .find(|a| a.conclusion == Literal::atom("q"))
741 .unwrap();
742 let nq_arg = built
743 .arguments
744 .iter()
745 .find(|a| a.conclusion == Literal::neg("q"))
746 .unwrap();
747
748 assert!(
751 built.framework.attackers(&q_arg.id).contains(&&nq_arg.id),
752 "B (built from p_hi) should defeat A (built from p_lo)"
753 );
754 assert!(
755 !built.framework.attackers(&nq_arg.id).contains(&&q_arg.id),
756 "A's attack on B must be filtered out: A ≺ B by Def 3.23 case 3 \
757 (premise strict, rules tied)"
758 );
759
760 let preferred = built.framework.preferred_extensions().unwrap();
761 assert_eq!(preferred.len(), 1);
762 assert!(preferred[0].contains(&nq_arg.id));
763 assert!(!preferred[0].contains(&q_arg.id));
764 }
765
766 #[test]
767 fn elitist_single_rule_frontier_still_works() {
768 let mut system = StructuredSystem::new();
774 system.kb_mut().add_ordinary(Literal::atom("p"));
775 system.kb_mut().add_ordinary(Literal::atom("q"));
776 let r_strong = system.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("x"));
777 let r_weak = system.add_defeasible_rule(vec![Literal::atom("q")], Literal::neg("x"));
778 system.prefer_rule(r_strong, r_weak).unwrap();
779
780 let built = system.build_framework().unwrap();
781 let preferred = built.framework.preferred_extensions().unwrap();
782 assert_eq!(preferred.len(), 1);
783 let ext = &preferred[0];
784 let x_arg = built
785 .arguments
786 .iter()
787 .find(|a| a.conclusion == Literal::atom("x"))
788 .unwrap();
789 let nx_arg = built
790 .arguments
791 .iter()
792 .find(|a| a.conclusion == Literal::neg("x"))
793 .unwrap();
794 assert!(ext.contains(&x_arg.id));
795 assert!(!ext.contains(&nx_arg.id));
796 }
797
798 #[test]
799 fn prefer_rule_rejects_cyclic_preferences() {
800 let mut system = StructuredSystem::new();
801 system.add_ordinary(Literal::atom("p"));
802 system.add_ordinary(Literal::atom("q"));
803 let r1 = system.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("x"));
804 let r2 = system.add_defeasible_rule(vec![Literal::atom("q")], Literal::atom("y"));
805 system.prefer_rule(r1, r2).unwrap();
806 let result = system.prefer_rule(r2, r1);
808 assert!(matches!(result, Err(crate::Error::Aspic(_))));
809 }
810
811 #[test]
812 fn prefer_rule_rejects_reflexive_preferences() {
813 let mut system = StructuredSystem::new();
814 system.add_ordinary(Literal::atom("p"));
815 let r1 = system.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("x"));
816 let result = system.prefer_rule(r1, r1);
817 assert!(matches!(result, Err(crate::Error::Aspic(_))));
818 }
819
820 #[test]
821 fn build_output_conclusions_in_extension_returns_literals() {
822 let mut sys = StructuredSystem::new();
823 sys.add_ordinary(Literal::atom("p"));
824 let _r = sys.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("q"));
825 let built = sys.build_framework().unwrap();
826 let grounded = built.framework.grounded_extension();
827 let concls = built.conclusions_in(&grounded);
828 assert!(concls.contains(&Literal::atom("p")));
829 assert!(concls.contains(&Literal::atom("q")));
830 assert_eq!(concls.len(), 2);
831 }
832
833 #[test]
834 fn build_output_argument_by_conclusion_finds_unique_matches() {
835 let mut sys = StructuredSystem::new();
836 sys.add_ordinary(Literal::atom("p"));
837 let _r = sys.add_defeasible_rule(vec![Literal::atom("p")], Literal::atom("q"));
838 let built = sys.build_framework().unwrap();
839 let q_arg = built.argument_by_conclusion(&Literal::atom("q"));
840 assert!(q_arg.is_some());
841 assert_eq!(q_arg.unwrap().conclusion, Literal::atom("q"));
842 let missing = built.argument_by_conclusion(&Literal::atom("never"));
843 assert!(missing.is_none());
844 }
845
846 #[test]
847 fn premise_preferences_support_transitive_closure() {
848 let mut sys = StructuredSystem::new();
849 sys.add_ordinary(Literal::atom("p"));
850 sys.add_ordinary(Literal::atom("q"));
851 sys.add_ordinary(Literal::atom("r"));
852 sys.prefer_premise(Literal::atom("p"), Literal::atom("q"))
853 .unwrap();
854 sys.prefer_premise(Literal::atom("q"), Literal::atom("r"))
855 .unwrap();
856 assert!(sys.is_premise_preferred(&Literal::atom("p"), &Literal::atom("r")));
857 assert!(!sys.is_premise_preferred(&Literal::atom("r"), &Literal::atom("p")));
858 }
859
860 #[test]
861 fn prefer_premise_rejects_reflexive() {
862 let mut sys = StructuredSystem::new();
863 sys.add_ordinary(Literal::atom("p"));
864 let result = sys.prefer_premise(Literal::atom("p"), Literal::atom("p"));
865 assert!(matches!(result, Err(crate::Error::Aspic(_))));
866 }
867
868 #[test]
869 fn prefer_premise_rejects_cyclic() {
870 let mut sys = StructuredSystem::new();
871 sys.add_ordinary(Literal::atom("p"));
872 sys.add_ordinary(Literal::atom("q"));
873 sys.prefer_premise(Literal::atom("p"), Literal::atom("q"))
874 .unwrap();
875 let result = sys.prefer_premise(Literal::atom("q"), Literal::atom("p"));
876 assert!(matches!(result, Err(crate::Error::Aspic(_))));
877 }
878
879 #[test]
880 fn premise_preference_blocks_undermine_when_target_premise_stronger() {
881 let mut sys = StructuredSystem::new();
890 sys.add_ordinary(Literal::atom("s"));
891 sys.add_ordinary(Literal::atom("u"));
892 sys.add_strict_rule(vec![Literal::atom("u")], Literal::neg("s"));
893 sys.prefer_premise(Literal::atom("s"), Literal::atom("u"))
894 .unwrap();
895
896 let built = sys.build_framework().unwrap();
897 let s_arg = built
898 .argument_by_conclusion(&Literal::atom("s"))
899 .expect("s premise argument");
900 let grounded = built.framework.grounded_extension();
901 assert!(
902 grounded.contains(&s_arg.id),
903 "expected s to survive under premise preference s > u, got {:?}",
904 grounded
905 );
906 }
907
908 #[test]
909 fn build_output_arguments_with_conclusion_returns_all_matches() {
910 let mut sys = StructuredSystem::new();
911 sys.add_ordinary(Literal::atom("a"));
912 sys.add_ordinary(Literal::atom("b"));
913 let _r1 = sys.add_defeasible_rule(vec![Literal::atom("a")], Literal::atom("target"));
914 let _r2 = sys.add_defeasible_rule(vec![Literal::atom("b")], Literal::atom("target"));
915 let built = sys.build_framework().unwrap();
916 let matches = built.arguments_with_conclusion(&Literal::atom("target"));
917 assert_eq!(matches.len(), 2);
918 }
919}