Spaces:
Sleeping
Sleeping
File size: 6,539 Bytes
1605cbb | 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 | """
FALSIFY core-logic tests — belief revision without any API key or database.
Each test builds a small graph via the :class:`~tests.conftest.FakeGraph` fixture,
runs the real cascade/forget code, and asserts on the resulting truth-states. These
cover the correctness cases enumerated in REQUIREMENTS.md §4.3:
* direct refutation (test_direct_refutation)
* forward cascade invalidates conclusion (test_forward_cascade_invalidates)
* diamond / partial refutation (test_diamond_partial_refutation)
* non-critical dependency survives (test_non_critical_dependency_survives)
* cycle safety (termination) (test_cycle_safety)
* surgical forget of orphans only (test_forget_orphan_keeps_provenance)
* hypothesis demote/promote (test_promote_competing_hypothesis)
"""
from __future__ import annotations
import pytest
from falsify.edges import DEPENDS_ON, SUPERSEDES, SUPPORTS
from falsify.models import TruthState
from falsify.tasks.cascade_forget import cascade_forget
from falsify.tasks.propagate_refutation import (
promote_competing_hypothesis,
propagate_refutation,
)
pytestmark = pytest.mark.asyncio
async def _alignment(g, nid: str):
entry = g.truth.get(str(nid))
return entry["truth_alignment"] if entry else None
async def test_direct_refutation(fake_graph):
"""Refuting an evidence node sets its truth_alignment to ['refuted']."""
g = fake_graph
g.add_node("E", claim="the March QA report documents the defect")
res = await propagate_refutation(["E"])
assert res.refuted == ["E"]
assert await _alignment(g, "E") == [TruthState.REFUTED.value]
async def test_forward_cascade_invalidates(fake_graph):
"""A→B→C depends_on chain: refuting the base evidence invalidates the chain.
Topology (depends_on points Conclusion→Evidence):
C --depends_on(critical)--> B --depends_on(critical)--> E(evidence)
Refuting E must invalidate B, then C.
"""
g = fake_graph
g.add_node("E", claim="base evidence")
g.add_node("B", statement="mid conclusion")
g.add_node("C", statement="top conclusion")
g.add_edge("B", "E", DEPENDS_ON, critical=True)
g.add_edge("C", "B", DEPENDS_ON, critical=True)
res = await propagate_refutation(["E"])
assert await _alignment(g, "E") == [TruthState.REFUTED.value]
assert await _alignment(g, "B") == [TruthState.INVALIDATED.value]
assert await _alignment(g, "C") == [TruthState.INVALIDATED.value]
assert set(res.invalidated) == {"B", "C"}
async def test_diamond_partial_refutation(fake_graph):
"""A conclusion with two critical supporters survives losing only one.
K depends_on E1 (critical) AND E2 (critical). Refuting only E1 must keep K alive
(E2 still supports it); refuting E2 as well then invalidates K.
"""
g = fake_graph
g.add_node("E1", claim="evidence one")
g.add_node("E2", claim="evidence two")
g.add_node("K", statement="conclusion on both")
g.add_edge("K", "E1", DEPENDS_ON, critical=True)
g.add_edge("K", "E2", DEPENDS_ON, critical=True)
# First refutation: K keeps an alive critical supporter (E2) -> stays alive.
await propagate_refutation(["E1"])
assert await _alignment(g, "E1") == [TruthState.REFUTED.value]
assert await _alignment(g, "K") == [TruthState.ALIVE.value]
# Second refutation: K loses its last critical supporter -> invalidated.
await propagate_refutation(["E2"])
assert await _alignment(g, "K") == [TruthState.INVALIDATED.value]
async def test_non_critical_dependency_survives(fake_graph):
"""Refuting a NON-critical dependency weakens but does not invalidate."""
g = fake_graph
g.add_node("E", claim="soft evidence")
g.add_node("K", statement="conclusion softly resting on E")
g.add_edge("K", "E", DEPENDS_ON, critical=False)
res = await propagate_refutation(["E"])
assert await _alignment(g, "K") == [TruthState.ALIVE.value]
assert "K" in res.weakened
assert "K" not in res.invalidated
async def test_cycle_safety(fake_graph):
"""A cyclic depends_on graph terminates (visited set) and doesn't hang."""
g = fake_graph
g.add_node("E", claim="evidence")
g.add_node("X", statement="X")
g.add_node("Y", statement="Y")
# Cycle among conclusions, all critically resting on E and each other.
g.add_edge("X", "E", DEPENDS_ON, critical=True)
g.add_edge("Y", "X", DEPENDS_ON, critical=True)
g.add_edge("X", "Y", DEPENDS_ON, critical=True)
res = await propagate_refutation(["E"]) # must return, not loop forever
assert await _alignment(g, "E") == [TruthState.REFUTED.value]
assert "X" in res.invalidated and "Y" in res.invalidated
async def test_forget_orphan_keeps_provenance(fake_graph):
"""cascade_forget deletes an orphaned invalidated conclusion but keeps the
refuted evidence that is a supersedes-anchor (provenance tombstone)."""
g = fake_graph
g.add_node("E", claim="refuted evidence")
g.add_node("K", statement="orphaned conclusion")
g.add_node("NEW", claim="the new fact")
g.add_edge("K", "E", DEPENDS_ON, critical=True)
g.add_edge("NEW", "E", SUPERSEDES, confidence=0.9) # NEW (alive) supersedes E
# Run the cascade, then forget.
prop = await propagate_refutation(["E"])
forget = await cascade_forget(prop.affected)
# K is orphaned (no alive consumer) -> deleted from both stores.
assert "K" in forget.forgotten
assert "K" in g.deleted
assert g.deleted_from_collections # vector collections were targeted
# E is refuted but retained as the supersedes provenance anchor.
assert "E" in forget.retained_provenance
assert "E" not in g.deleted
async def test_promote_competing_hypothesis(fake_graph):
"""When A's only support dies, A is superseded and rival B is promoted."""
g = fake_graph
g.add_node("E_a", claim="evidence for A")
g.add_node("E_b", claim="evidence for B")
g.add_node("A", statement="hypothesis A")
g.add_node("B", statement="hypothesis B")
g.add_edge("E_a", "A", SUPPORTS, weight=0.8)
g.add_edge("E_b", "B", SUPPORTS, weight=0.7)
# Refute A's evidence, then re-score hypotheses.
prop = await propagate_refutation(["E_a"])
actions = await promote_competing_hypothesis(["E_a"], prop.epoch)
assert actions.get("A") == "superseded"
assert actions.get("B") == "promoted"
assert await _alignment(g, "A") == [TruthState.SUPERSEDED.value]
|