"""Derivation engine: provenance, confidence, claim chains, cascade invalidation on refutation, and explainability.""" import json from helpers import ( claim_dependents, closure, get_all_claims, load_everything, op_index, status_of, ) def test_derivation_produces_claims_with_provenance(phone): load_everything(phone) claims = get_all_claims(phone) assert len(claims) >= 3, "sample datasets should yield several claims" idx = op_index(phone) for claim in claims: assert claim["id"] in idx, "every claim must exist as an op in the log" assert claim.get("sources"), f"claim {claim['id']} has no provenance" for source in claim["sources"]: assert source in idx, f"claim {claim['id']} has dangling source {source}" confidence = float(claim["confidence"]) assert 0.0 < confidence <= 1.0 def test_claims_start_active(phone): load_everything(phone) for claim in get_all_claims(phone): assert claim.get("status", "active") == "active" def test_some_claim_derives_from_another_claim(phone): load_everything(phone) deps = claim_dependents(get_all_claims(phone), op_index(phone)) assert deps, ( "the sample datasets should produce at least one claim derived from " "another claim (a derivation chain), otherwise cascade cannot be shown" ) def test_refutation_cascades_to_dependents(phone): load_everything(phone) claims = get_all_claims(phone) idx = op_index(phone) deps = claim_dependents(claims, idx) assert deps base = sorted(deps)[0] downstream = closure(deps, base) assert downstream, "chosen base claim should have at least one dependent" phone.refute(base, reason="user says this is wrong") assert status_of(phone, base) != "active", "refuted claim must not stay active" for claim_id in downstream: assert status_of(phone, claim_id) != "active", ( f"dependent claim {claim_id} survived refutation of its source" ) untouched = [ c["id"] for c in claims if c["id"] != base and c["id"] not in downstream ] if untouched: assert any(status_of(phone, cid) == "active" for cid in untouched), ( "refutation must not invalidate unrelated claims" ) def test_refutation_is_recorded_in_the_log(phone): load_everything(phone) claims = get_all_claims(phone) target = sorted(claims, key=lambda c: c["id"])[0] before = {op["id"] for op in phone.store.all_ops()} phone.refute(target["id"], reason="test refutation") after = {op["id"] for op in phone.store.all_ops()} new_ops = after - before assert new_ops, "refutation must append at least one signed op to the log" idx = op_index(phone) assert any(idx[op_id].get("type") != "claim" for op_id in new_ops) def test_explain_links_claim_to_its_sources(phone): load_everything(phone) claims = get_all_claims(phone) target = sorted(claims, key=lambda c: c["id"])[0] explanation = phone.explain(target["id"]) assert explanation, "explain() must return a non-empty structure" blob = json.dumps(explanation, default=str) assert target["sources"][0] in blob, ( "the explanation must reference the claim's provenance sources" )