-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathtest_formal_verification_fix.py
More file actions
655 lines (535 loc) · 24.9 KB
/
test_formal_verification_fix.py
File metadata and controls
655 lines (535 loc) · 24.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
"""
Tests for the formal_verification auto-prove fix (Issue #7).
Verifies that:
1. _parse_formula rejects bad input with FormulaParseError (not opaque symbols)
2. Ungrounded conclusions are rejected by all proof methods
3. Valid logical entailments succeed
4. Invalid logical fallacies fail
5. EthicalVerificationEngine actually checks constraints
6. Axioms parse correctly and participate in proofs
7. Model checking enumerates all truth assignments
"""
import pytest
import sympy as sp
from datetime import datetime, timezone
from sympy.logic.boolalg import And, Implies, Not, Or
from asi_build.safety.formal_verification import (
EthicalAxiom,
EthicalConstraint,
EthicalPrinciple,
EthicalVerificationEngine,
FormalProof,
FormulaParseError,
LogicalPredicate,
TheoremProver,
parse_logic_formula,
_get_symbol,
)
# ============================================================
# Section 1: parse_logic_formula — the root-cause fix
# ============================================================
class TestParseLogicFormula:
"""Verify that the shared formula parser is sound."""
def test_simple_symbol(self):
expr = parse_logic_formula("A")
assert isinstance(expr, sp.Symbol)
assert str(expr) == "A"
def test_negation(self):
expr = parse_logic_formula("~A")
assert isinstance(expr, Not)
def test_conjunction(self):
expr = parse_logic_formula("A & B")
assert isinstance(expr, And)
def test_disjunction(self):
expr = parse_logic_formula("A | B")
assert isinstance(expr, Or)
def test_implies_arrow(self):
expr = parse_logic_formula("A -> B")
assert isinstance(expr, Implies)
def test_implies_keyword(self):
expr = parse_logic_formula("A implies B")
assert isinstance(expr, Implies)
def test_implies_double_gt(self):
expr = parse_logic_formula("A >> B")
assert isinstance(expr, Implies)
def test_complex_formula(self):
expr = parse_logic_formula("A -> (B & C)")
assert isinstance(expr, Implies)
# consequent should be And(B, C)
assert isinstance(expr.args[1], And)
def test_not_keyword(self):
expr = parse_logic_formula("not A")
assert isinstance(expr, Not)
def test_and_keyword(self):
expr = parse_logic_formula("A and B")
assert isinstance(expr, And)
def test_or_keyword(self):
expr = parse_logic_formula("A or B")
assert isinstance(expr, Or)
def test_function_call_syntax_converted(self):
"""P(x) should be converted to flat symbol P_x, not crash."""
expr = parse_logic_formula("P_x")
assert isinstance(expr, sp.Symbol)
def test_quantifier_stripped(self):
"""Quantifiers are stripped; body is parsed as propositional."""
expr = parse_logic_formula("forall x: P_x")
assert isinstance(expr, sp.Symbol)
assert str(expr) == "P_x"
def test_shared_symbol_registry(self):
"""Same variable name always produces the same Symbol object."""
expr1 = parse_logic_formula("foo")
expr2 = parse_logic_formula("foo & bar")
# 'foo' in expr2 should be identical to expr1
foo_in_expr2 = [s for s in expr2.free_symbols if str(s) == "foo"][0]
assert expr1 is foo_in_expr2
def test_rejects_numeric_result(self):
"""A formula that evaluates to a number is rejected."""
with pytest.raises(FormulaParseError):
parse_logic_formula("2 + 3")
def test_nested_parentheses(self):
expr = parse_logic_formula("(A | B) & (C -> D)")
assert isinstance(expr, And)
def test_double_negation(self):
expr = parse_logic_formula("~~A")
# SymPy simplifies ~~A to A
assert expr == _get_symbol("A")
def test_precedence_implies_and(self):
"""'A -> B & C' should parse as '(A -> B) & C' per SymPy precedence."""
expr = parse_logic_formula("A -> B & C")
# This is And(Implies(A, B), C) in SymPy
assert isinstance(expr, And)
def test_explicit_parenthesisation(self):
"""'A -> (B & C)' should correctly group the consequent."""
expr = parse_logic_formula("A -> (B & C)")
assert isinstance(expr, Implies)
assert isinstance(expr.args[1], And)
# ============================================================
# Section 2: Auto-prove blocking
# ============================================================
class TestAutoProveBlocked:
"""The core issue: unrelated / ungrounded conclusions must NOT prove."""
def test_unrelated_conclusion_resolution(self):
"""Conclusion with no relation to premises → must fail."""
prover = TheoremProver()
proof = prover.prove_theorem(["A", "B"], "totally_unrelated", method="resolution")
assert proof.validity is False
def test_unrelated_conclusion_model_checking(self):
prover = TheoremProver()
proof = prover.prove_theorem(["A", "B"], "xyz", method="model_checking")
assert proof.validity is False
def test_unrelated_conclusion_natural_deduction(self):
prover = TheoremProver()
proof = prover.prove_theorem(["A", "B"], "xyz", method="natural_deduction")
assert proof.validity is False
def test_ungrounded_symbol_rejected_resolution(self):
"""Conclusion containing symbols not in any premise → must fail."""
prover = TheoremProver()
# A is in premises, but C is NOT
proof = prover.prove_theorem(["A"], "A & C", method="resolution")
assert proof.validity is False
def test_ungrounded_symbol_rejected_model_checking(self):
prover = TheoremProver()
proof = prover.prove_theorem(["A"], "A & C", method="model_checking")
assert proof.validity is False
def test_ungrounded_with_axioms(self):
"""Even with axioms loaded, ungrounded conclusions must fail."""
prover = TheoremProver()
prover.add_axiom(EthicalAxiom("impl", "A -> B", "A implies B"))
# C is not in A, B, or the axiom
proof = prover.prove_theorem(["A"], "C", method="resolution")
assert proof.validity is False
def test_same_unparseable_formula_no_longer_auto_proves(self):
"""Previously: same bad formula in premise+conclusion → auto-proved
because both became the same opaque Symbol. Now parse_logic_formula
should either parse it correctly or raise FormulaParseError."""
prover = TheoremProver()
# 'forall x: P(x)' would fail in the old parser → same opaque symbol
# Now quantifiers are stripped and P(x) → P_x symbol
proof = prover.prove_theorem(["forall x: P_x"], "forall x: P_x", method="resolution")
# This should still be valid (premise = conclusion), but for the RIGHT reason
assert proof.validity is True
def test_no_premises_cannot_prove_anything(self):
"""Empty premises should not prove any non-tautological conclusion."""
prover = TheoremProver()
proof = prover.prove_theorem([], "A", method="resolution")
assert proof.validity is False
# ============================================================
# Section 3: Valid logical entailments
# ============================================================
class TestValidEntailments:
"""Standard logical entailments that MUST succeed."""
def test_modus_ponens_resolution(self):
"""(P, P→Q) ⊢ Q"""
prover = TheoremProver()
prover.add_axiom(EthicalAxiom("mp", "P -> Q", "P implies Q"))
proof = prover.prove_theorem(["P"], "Q", method="resolution")
assert proof.validity is True
def test_modus_ponens_model_checking(self):
prover = TheoremProver()
proof = prover.prove_theorem(["P", "P -> Q"], "Q", method="model_checking")
assert proof.validity is True
def test_modus_ponens_natural_deduction(self):
prover = TheoremProver()
prover.add_axiom(EthicalAxiom("mp", "P -> Q", "P implies Q"))
proof = prover.prove_theorem(["P"], "Q", method="natural_deduction")
assert proof.validity is True
def test_modus_tollens(self):
"""(P→Q, ~Q) ⊢ ~P"""
prover = TheoremProver()
proof = prover.prove_theorem(["P -> Q", "~Q"], "~P", method="resolution")
assert proof.validity is True
def test_hypothetical_syllogism(self):
"""(P→Q, Q→R) ⊢ P→R"""
prover = TheoremProver()
proof = prover.prove_theorem(["P -> Q", "Q -> R"], "P -> R", method="resolution")
assert proof.validity is True
def test_disjunctive_syllogism(self):
"""(P|Q, ~P) ⊢ Q"""
prover = TheoremProver()
proof = prover.prove_theorem(["P | Q", "~P"], "Q", method="resolution")
assert proof.validity is True
def test_conjunction_from_premises(self):
"""(A, B) ⊢ A & B"""
prover = TheoremProver()
proof = prover.prove_theorem(["A", "B"], "A & B", method="resolution")
assert proof.validity is True
def test_simplification(self):
"""(A & B) ⊢ A"""
prover = TheoremProver()
proof = prover.prove_theorem(["A & B"], "A", method="resolution")
assert proof.validity is True
def test_double_negation(self):
"""(~~P) ⊢ P"""
prover = TheoremProver()
proof = prover.prove_theorem(["~~P"], "P", method="resolution")
assert proof.validity is True
def test_identity(self):
"""(P) ⊢ P"""
prover = TheoremProver()
proof = prover.prove_theorem(["P"], "P", method="resolution")
assert proof.validity is True
def test_chained_implication(self):
"""A, A→B, B→C ⊢ C"""
prover = TheoremProver()
proof = prover.prove_theorem(["A", "A -> B", "B -> C"], "C", method="resolution")
assert proof.validity is True
def test_contrapositive(self):
"""(A→B) ⊢ (~B→~A)"""
prover = TheoremProver()
proof = prover.prove_theorem(["A -> B"], "~B -> ~A", method="resolution")
assert proof.validity is True
# ============================================================
# Section 4: Invalid logical fallacies — must FAIL
# ============================================================
class TestInvalidFallacies:
"""Standard logical fallacies that MUST fail."""
def test_affirming_the_consequent(self):
"""(Q, P→Q) ⊬ P — classic fallacy"""
prover = TheoremProver()
prover.add_axiom(EthicalAxiom("impl", "P -> Q", "P implies Q"))
proof = prover.prove_theorem(["Q"], "P", method="resolution")
assert proof.validity is False
def test_denying_the_antecedent(self):
"""(~P, P→Q) ⊬ ~Q — classic fallacy"""
prover = TheoremProver()
proof = prover.prove_theorem(["~P", "P -> Q"], "~Q", method="resolution")
assert proof.validity is False
def test_converse_error(self):
"""(P→Q) ⊬ (Q→P)"""
prover = TheoremProver()
proof = prover.prove_theorem(["P -> Q"], "Q -> P", method="resolution")
assert proof.validity is False
def test_non_sequitur(self):
"""(A) ⊬ B when there's no logical link"""
prover = TheoremProver()
proof = prover.prove_theorem(["A"], "B", method="resolution")
assert proof.validity is False
def test_invalid_strengthening(self):
"""(A | B) ⊬ A & B"""
prover = TheoremProver()
proof = prover.prove_theorem(["A | B"], "A & B", method="resolution")
assert proof.validity is False
def test_affirming_consequent_model_checking(self):
"""Same fallacy should fail under model checking too."""
prover = TheoremProver()
proof = prover.prove_theorem(["Q", "P -> Q"], "P", method="model_checking")
assert proof.validity is False
def test_denying_antecedent_model_checking(self):
prover = TheoremProver()
proof = prover.prove_theorem(["~P", "P -> Q"], "~Q", method="model_checking")
assert proof.validity is False
# ============================================================
# Section 5: Contradiction handling
# ============================================================
class TestContradiction:
"""Contradictory premises should not prove arbitrary ungrounded things."""
def test_contradiction_does_not_prove_ungrounded(self):
"""(A, ~A) ⊬ B — B is ungrounded, safety check blocks ex falso."""
prover = TheoremProver()
proof = prover.prove_theorem(["A", "~A"], "B", method="resolution")
assert proof.validity is False
def test_contradiction_rejects_grounded_conclusion(self):
"""(A, ~A) ⊬ A — contradictory premises are rejected outright.
While classically valid (ex falso), this is rejected for safety:
contradictory premises should signal an error, not prove things.
"""
prover = TheoremProver()
proof = prover.prove_theorem(["A", "~A"], "A", method="resolution")
assert proof.validity is False
def test_contradiction_in_premises_detected(self):
"""Verify that ~Y from hypothesis [~Y] doesn't prove Y."""
prover = TheoremProver()
proof = prover.prove_theorem(["~Y"], "Y", method="resolution")
assert proof.validity is False
# ============================================================
# Section 6: Axiom parsing
# ============================================================
class TestAxiomParsing:
"""Verify that all default axioms parse into real SymPy expressions."""
def test_all_axioms_are_not_true_or_opaque(self):
"""No axiom should degenerate to sp.true or an opaque Symbol."""
engine = EthicalVerificationEngine()
for axiom in engine.theorem_prover.axioms:
expr = axiom.to_sympy()
assert expr != sp.true, f"Axiom {axiom.name!r} parsed to sp.true"
assert not (
isinstance(expr, sp.Symbol) and " " in str(expr)
), f"Axiom {axiom.name!r} is an opaque symbol: {expr!r}"
def test_axioms_have_free_symbols(self):
"""Each axiom should reference propositional variables."""
engine = EthicalVerificationEngine()
for axiom in engine.theorem_prover.axioms:
expr = axiom.to_sympy()
assert len(expr.free_symbols) > 0, (
f"Axiom {axiom.name!r} has no free symbols — probably parsed wrong"
)
def test_axiom_autonomy_is_implication(self):
axiom = EthicalAxiom("test", "respects_autonomy -> ~violates_autonomy", "")
expr = axiom.to_sympy()
assert isinstance(expr, Implies)
def test_axiom_participates_in_proof(self):
"""Axiom should actually enable proofs that wouldn't work without it."""
prover = TheoremProver()
# Without axiom, P ⊬ Q
proof_no_axiom = prover.prove_theorem(["P"], "Q", method="resolution")
assert proof_no_axiom.validity is False
prover.add_axiom(EthicalAxiom("bridge", "P -> Q", "P implies Q"))
proof_with_axiom = prover.prove_theorem(["P"], "Q", method="resolution")
assert proof_with_axiom.validity is True
# ============================================================
# Section 7: Model checking thoroughness
# ============================================================
class TestModelChecking:
"""Verify exhaustive enumeration catches counterexamples."""
def test_finds_counterexample_affirming_consequent(self):
"""Model checker should find: P=False, Q=True as counterexample."""
prover = TheoremProver()
result = prover._prove_by_model_checking(["Q", "P -> Q"], "P")
assert result["valid"] is False
assert len(result["counter_examples"]) > 0
def test_all_models_checked(self):
"""For 2 variables, there are 4 models. The old code only checked 2."""
prover = TheoremProver()
# A | B ⊬ A — the counterexample is A=False, B=True
result = prover._prove_by_model_checking(["A | B"], "A")
assert result["valid"] is False
# The old code checked only all-True and all-False, missing A=F,B=T
def test_valid_tautology(self):
"""A | ~A should be valid in all models."""
prover = TheoremProver()
proof = prover.prove_theorem(["A | ~A"], "A | ~A", method="model_checking")
assert proof.validity is True
def test_model_checking_with_axioms(self):
"""Axioms should participate in model checking too."""
prover = TheoremProver()
prover.add_axiom(EthicalAxiom("bridge", "A -> B", "A implies B"))
proof = prover.prove_theorem(["A"], "B", method="model_checking")
assert proof.validity is True
# ============================================================
# Section 8: Ethical engine actually checks constraints
# ============================================================
class TestEthicalEngineActuallyChecks:
"""The ethical verification engine must reject actually-unethical proposals."""
def _make_constraint(self, cid, name, principle, spec, predicates=None):
return EthicalConstraint(
id=cid,
name=name,
description=name,
principle=principle,
formal_specification=spec,
predicates=predicates or [],
quantifiers={},
priority=10,
created_at=datetime.now(tz=timezone.utc),
)
def test_harmful_proposal_rejected(self):
"""Proposal that causes harm must fail ~causes_harm constraint."""
engine = EthicalVerificationEngine()
engine.add_constraint(
self._make_constraint(
"harm", "No Harm", EthicalPrinciple.NON_MALEFICENCE, "~causes_harm"
)
)
result = engine.verify_proposal_ethics({
"causes_harm": True,
"impact_assessment": {"harm_level": 0.9},
})
assert result["overall_valid"] is False
assert result["constraint_results"]["harm"]["valid"] is False
def test_safe_proposal_accepted(self):
"""Proposal that does not cause harm should pass ~causes_harm."""
engine = EthicalVerificationEngine()
engine.add_constraint(
self._make_constraint(
"harm", "No Harm", EthicalPrinciple.NON_MALEFICENCE, "~causes_harm"
)
)
result = engine.verify_proposal_ethics({
"causes_harm": False,
"impact_assessment": {"harm_level": 0},
})
assert result["overall_valid"] is True
assert result["constraint_results"]["harm"]["valid"] is True
def test_transparency_constraint_fails_when_not_transparent(self):
"""Proposal affecting stakeholders but not transparent should fail."""
engine = EthicalVerificationEngine()
engine.add_constraint(
self._make_constraint(
"trans", "Transparency", EthicalPrinciple.TRANSPARENCY, "is_transparent"
)
)
result = engine.verify_proposal_ethics({
"is_transparent": False,
"impact_assessment": {"affected_parties": ["users"]},
})
assert result["overall_valid"] is False
def test_transparency_constraint_passes_when_transparent(self):
engine = EthicalVerificationEngine()
engine.add_constraint(
self._make_constraint(
"trans", "Transparency", EthicalPrinciple.TRANSPARENCY, "is_transparent"
)
)
result = engine.verify_proposal_ethics({
"is_transparent": True,
"impact_assessment": {"affected_parties": ["users"]},
})
assert result["overall_valid"] is True
def test_multiple_constraints_all_must_pass(self):
"""If one constraint fails, overall_valid is False."""
engine = EthicalVerificationEngine()
engine.add_constraint(
self._make_constraint(
"harm", "No Harm", EthicalPrinciple.NON_MALEFICENCE, "~causes_harm"
)
)
engine.add_constraint(
self._make_constraint(
"fair", "Fairness", EthicalPrinciple.FAIRNESS, "is_fair"
)
)
result = engine.verify_proposal_ethics({
"causes_harm": False,
"is_fair": False, # fails fairness
})
assert result["overall_valid"] is False
assert result["constraint_results"]["harm"]["valid"] is True
assert result["constraint_results"]["fair"]["valid"] is False
def test_all_constraints_pass(self):
engine = EthicalVerificationEngine()
engine.add_constraint(
self._make_constraint(
"harm", "No Harm", EthicalPrinciple.NON_MALEFICENCE, "~causes_harm"
)
)
engine.add_constraint(
self._make_constraint(
"fair", "Fairness", EthicalPrinciple.FAIRNESS, "is_fair"
)
)
result = engine.verify_proposal_ethics({
"causes_harm": False,
"is_fair": True,
})
assert result["overall_valid"] is True
def test_complex_constraint_with_axiom_interaction(self):
"""Test that axioms interact with constraints.
Axiom: is_beneficial -> promotes_wellbeing
Constraint: requires promotes_wellbeing
Proposal: is_beneficial=True
The axiom should enable deriving promotes_wellbeing from is_beneficial.
"""
engine = EthicalVerificationEngine()
engine.add_constraint(
self._make_constraint(
"benefit", "Benefit", EthicalPrinciple.BENEFICENCE, "promotes_wellbeing"
)
)
result = engine.verify_proposal_ethics({
"is_beneficial": True,
})
# The "beneficence" axiom says: is_beneficial -> promotes_wellbeing
# So from is_beneficial=True, we should derive promotes_wellbeing=True
assert result["constraint_results"]["benefit"]["valid"] is True
def test_axiom_cannot_prove_without_matching_facts(self):
"""Axiom is_beneficial->promotes_wellbeing should NOT fire when
is_beneficial is False."""
engine = EthicalVerificationEngine()
engine.add_constraint(
self._make_constraint(
"benefit", "Benefit", EthicalPrinciple.BENEFICENCE, "promotes_wellbeing"
)
)
result = engine.verify_proposal_ethics({
"is_beneficial": False,
})
# is_beneficial=False, so axiom antecedent is false,
# promotes_wellbeing is unconstrained → not provable
assert result["constraint_results"]["benefit"]["valid"] is False
# ============================================================
# Section 9: FormulaParseError — no silent failures
# ============================================================
class TestFormulaParseError:
"""The parser must raise FormulaParseError, never silently return garbage."""
def test_parse_error_is_raised_not_swallowed(self):
"""Unparseable formulas raise FormulaParseError."""
# This would previously return sp.Symbol("...") silently
with pytest.raises(FormulaParseError):
parse_logic_formula("2 + 3")
def test_prove_theorem_catches_parse_error(self):
"""TheoremProver.prove_theorem wraps errors in validity=False proof."""
prover = TheoremProver()
# The prove_theorem method has a try/except that catches all errors
# and returns a proof with validity=False
proof = prover.prove_theorem(["2 + 3"], "A", method="resolution")
assert proof.validity is False
# ============================================================
# Section 10: Natural deduction symbolic
# ============================================================
class TestNaturalDeductionSymbolic:
"""Verify the symbolic natural deduction engine."""
def test_modus_ponens(self):
prover = TheoremProver()
prover.add_axiom(EthicalAxiom("mp", "A -> B", ""))
proof = prover.prove_theorem(["A"], "B", method="natural_deduction")
assert proof.validity is True
def test_chained_modus_ponens(self):
prover = TheoremProver()
prover.add_axiom(EthicalAxiom("ab", "A -> B", ""))
prover.add_axiom(EthicalAxiom("bc", "B -> C", ""))
proof = prover.prove_theorem(["A"], "C", method="natural_deduction")
assert proof.validity is True
def test_modus_tollens_natural_deduction(self):
prover = TheoremProver()
prover.add_axiom(EthicalAxiom("impl", "A -> B", ""))
proof = prover.prove_theorem(["~B"], "~A", method="natural_deduction")
assert proof.validity is True
def test_simplification(self):
"""Natural deduction should be able to simplify conjunctions."""
prover = TheoremProver()
proof = prover.prove_theorem(["A & B"], "A", method="natural_deduction")
assert proof.validity is True
def test_ungrounded_fails_natural_deduction(self):
prover = TheoremProver()
proof = prover.prove_theorem(["A"], "Z", method="natural_deduction")
assert proof.validity is False